From 7b1d21c85eba6d70975af0307b813a786574c04d Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Thu, 23 Oct 2025 10:02:02 +0200 Subject: [PATCH 1/9] Service agents cursed knowledge (#3455) --- CURSED_KNOWLEDGE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CURSED_KNOWLEDGE.md b/CURSED_KNOWLEDGE.md index d33083556..f3b12e21b 100644 --- a/CURSED_KNOWLEDGE.md +++ b/CURSED_KNOWLEDGE.md @@ -2,6 +2,7 @@ +* 2025-10-23 Some [service agents](https://cloud.google.com/iam/docs/service-agents) are not created upon API activation nor calling `google_project_service_identity`. Since we have no way of knowing if they exist, we avoid automatically granting their respective roles in the project module. The list of agents for which we do not perform automatic grants can be found in the [tools/build_service_agents.py](./tools/build_service_agents.py) script. * 2025-10-21 Type checking in ternaries requires both sides to have identical types. For objects, it means that they need to define the same fields. And sometimes `null` and `tonumber(null)` don't converge to a common type (citation needed) * 2025-10-21 You can't call a provider function within brackets [opentofu#3401](https://github.com/opentofu/opentofu/issues/3401) * 2025-10-21 Terraform dependency graph considers a variable or a local as one node in the graph [adrs/20251013-context-locals.md], you may resolve your dependency cycles by just rearranging your variables / locals. From c444c405fad06897f389ee683423bddd6c834574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Thu, 23 Oct 2025 07:54:49 +0000 Subject: [PATCH 2/9] More knowledge --- CURSED_KNOWLEDGE.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/CURSED_KNOWLEDGE.md b/CURSED_KNOWLEDGE.md index f3b12e21b..a228231a0 100644 --- a/CURSED_KNOWLEDGE.md +++ b/CURSED_KNOWLEDGE.md @@ -3,7 +3,23 @@ * 2025-10-23 Some [service agents](https://cloud.google.com/iam/docs/service-agents) are not created upon API activation nor calling `google_project_service_identity`. Since we have no way of knowing if they exist, we avoid automatically granting their respective roles in the project module. The list of agents for which we do not perform automatic grants can be found in the [tools/build_service_agents.py](./tools/build_service_agents.py) script. +* 2025-10-23 Use `terraform plan` after `terraform apply` to confirm that the plan is empty after applying the changes. Non-empty plan is a sign of potential bug in either Terraform code or provider and suggests, that configuration might not have been applied as expected or potential problems when implementing future changes +* 2025-10-23 Do not use `data` resource. Even if you must, then still it might be [a bad idea](#avoid-data-resources) +* 2025-10-23 when referring other resource prefer using `.id` attribute over names. `.id` is computed field, and will force update when referred resource is replaced. Sometimes this requires explicit `depends_on` - for example for Cloud Run IAM, so it is recreated when parent resource is replaced +* 2025-10-23 Maps are the best drivers for `for_each` on the resource level. When using lists, and adding something in the middle of list means that all resources following insertion needs to be replaced * 2025-10-21 Type checking in ternaries requires both sides to have identical types. For objects, it means that they need to define the same fields. And sometimes `null` and `tonumber(null)` don't converge to a common type (citation needed) -* 2025-10-21 You can't call a provider function within brackets [opentofu#3401](https://github.com/opentofu/opentofu/issues/3401) -* 2025-10-21 Terraform dependency graph considers a variable or a local as one node in the graph [adrs/20251013-context-locals.md], you may resolve your dependency cycles by just rearranging your variables / locals. +* 2025-10-21 Terraform dependency graph considers a variable or a local as one node in the graph [adrs/20251013-context-locals.md], you may resolve your dependency cycles by just rearranging your variables / locals. But for resources - the dependency is tracked on attribute level and plan may differ depending on which attribute you depend * 2025-10-21 `create_before_destory` meta-argument is [contagious](https://github.com/hashicorp/terraform/blob/main/docs/destroying.md#forced-create-before-destroy), which means - any resource that any resource depending on CBD resource will also be marked as CBD. This hits hard, when affected resource is silently accepting creation with the same name, even if the object exists (`google_storage_bucket_object`, I'm looking at you) + + +## Detailed explanations +### Avoid `data` resources +There are two problems, when using `data` resources: +* when reading is deferred to during apply, any values it returns are also `known after apply`, which may result in unnecessary resource replacement +* when deploying more complex infrastructure with `data` resources, and your deployment fails in the middle, it might be not possible to recover without manual intervention in what is configured, so `data` resource can read its values + +What is considered a safe use case for `data` resource: +* using it for validating invariants (resource is guaranteed to exist across full lifecycle of the state) +* using `data` resource outputs in attributes without `ForceNew` flag - so even if `data` will be read during apply, it will result in spurious update-in-place instead of replacement + +In Fabric FAST modules `data` resources are used only by request to simplify calling, but then the above caveats apply to the whole module. From e17d2d1dc559502a655eb91852026d58d0e3d07a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Sun, 19 Oct 2025 10:43:08 +0000 Subject: [PATCH 3/9] Cloud Function v1 - contexts and service account interface refactor --- modules/cloud-function-v1/README.md | 87 +++++++----- modules/cloud-function-v1/bundle.tf | 29 ++-- modules/cloud-function-v1/main.tf | 58 ++++---- modules/cloud-function-v1/serviceaccount.tf | 53 ++++++++ .../variables-serviceaccount.tf | 31 +++++ modules/cloud-function-v1/variables.tf | 41 +++--- .../examples/bucket-creation.yaml | 3 +- .../cloud_function_v1/examples/cmek.yaml | 73 ++++++++++ .../examples/custom-bundle.yaml | 113 ++++++++++++++++ .../examples/http-trigger.yaml | 115 ++++++++++++++++ .../cloud_function_v1/examples/iam.yaml | 6 +- .../examples/multiple_functions.yaml | 5 +- .../examples/private-build-pool.yaml | 125 ++++++++++++++++++ .../examples/pubsub-non-http-trigger.yaml | 84 ++++++++++++ .../cloud_function_v1/examples/secrets.yaml | 2 - .../examples/service-account.yaml | 75 +++++++++++ tests/modules/cloud_function_v1/kms.tfvars | 11 ++ tests/modules/cloud_function_v1/kms.yaml | 39 ++++++ tests/modules/cloud_function_v1/tftest.yaml | 17 +++ 19 files changed, 871 insertions(+), 96 deletions(-) create mode 100644 modules/cloud-function-v1/serviceaccount.tf create mode 100644 modules/cloud-function-v1/variables-serviceaccount.tf create mode 100644 tests/modules/cloud_function_v1/examples/cmek.yaml create mode 100644 tests/modules/cloud_function_v1/examples/custom-bundle.yaml create mode 100644 tests/modules/cloud_function_v1/examples/http-trigger.yaml create mode 100644 tests/modules/cloud_function_v1/examples/private-build-pool.yaml create mode 100644 tests/modules/cloud_function_v1/examples/pubsub-non-http-trigger.yaml create mode 100644 tests/modules/cloud_function_v1/examples/service-account.yaml create mode 100644 tests/modules/cloud_function_v1/kms.tfvars create mode 100644 tests/modules/cloud_function_v1/kms.yaml create mode 100644 tests/modules/cloud_function_v1/tftest.yaml diff --git a/modules/cloud-function-v1/README.md b/modules/cloud-function-v1/README.md index 551c798a6..47baab320 100644 --- a/modules/cloud-function-v1/README.md +++ b/modules/cloud-function-v1/README.md @@ -28,7 +28,7 @@ Cloud Function management, with support for IAM roles, optional bucket creation ### HTTP trigger -This deploys a Cloud Function with an HTTP endpoint, using a pre-existing GCS bucket for deployment, setting the service account to the Cloud Function default one, and delegating access control to the containing project. +This deploys a Cloud Function with an HTTP endpoint, using a pre-existing GCS bucket for deployment, creating service account dedicated for this function, granting it `roles/logging.logWriter` and `roles/monitoring.metricWriter` roles, and delegating access control to the containing project. ```hcl module "cf-http" { @@ -44,7 +44,7 @@ module "cf-http" { google_project_iam_member.bucket_default_compute_account_grant, ] } -# tftest modules=1 resources=5 fixtures=fixtures/functions-default-sa-iam-grants.tf e2e +# tftest fixtures=fixtures/functions-default-sa-iam-grants.tf inventory=http-trigger.yaml e2e ``` ### PubSub and non-HTTP triggers @@ -70,7 +70,7 @@ module "cf-http" { ] } -# tftest modules=2 resources=7 fixtures=fixtures/pubsub.tf,fixtures/functions-default-sa-iam-grants.tf e2e +# tftest inventory=pubsub-non-http-trigger.yaml fixtures=fixtures/pubsub.tf,fixtures/functions-default-sa-iam-grants.tf e2e ``` ### Controlling HTTP access @@ -125,7 +125,7 @@ module "cf-http" { ### Service account management -To use a custom service account managed by the module, set `service_account_create` to `true` and leave `service_account` set to `null` value (default). +To use a custom service account managed by the module, set `service_account_config.create` to `true`. ```hcl module "cf-http" { @@ -137,15 +137,17 @@ module "cf-http" { bundle_config = { path = "assets/sample-function/" } - service_account_create = true + service_account_config = { + create = true + } depends_on = [ google_project_iam_member.bucket_default_compute_account_grant, ] } -# tftest modules=1 resources=6 fixtures=fixtures/functions-default-sa-iam-grants.tf e2e +# tftest inventory=service-account.yaml fixtures=fixtures/functions-default-sa-iam-grants.tf e2e ``` -To use an externally managed service account, pass its email in `service_account` and leave `service_account_create` to `false` (the default). +To use an externally managed service account, pass its email in `service_account_config.email` and set `service_account_config.create` to `false`. ```hcl module "cf-http" { @@ -157,7 +159,10 @@ module "cf-http" { bundle_config = { path = "assets/sample-function/" } - service_account = var.service_account.email + service_account_config = { + create = false + email = var.service_account.email + } depends_on = [ google_project_iam_member.bucket_default_compute_account_grant, ] @@ -193,7 +198,7 @@ module "cf-http" { google_project_iam_member.bucket_default_compute_account_grant, ] } -# tftest modules=1 resources=5 fixtures=fixtures/functions-default-sa-iam-grants.tf e2e +# tftest inventory=custom-bundle.yaml fixtures=fixtures/functions-default-sa-iam-grants.tf e2e ``` ### Private Cloud Build Pool @@ -215,7 +220,7 @@ module "cf-http" { google_project_iam_member.bucket_default_compute_account_grant, ] } -# tftest modules=1 resources=6 fixtures=fixtures/cloudbuild-custom-pool.tf,fixtures/functions-default-sa-iam-grants.tf e2e +# tftest inventory=private-build-pool.yaml fixtures=fixtures/cloudbuild-custom-pool.tf,fixtures/functions-default-sa-iam-grants.tf e2e ``` ### Multiple Cloud Functions within project @@ -256,6 +261,8 @@ This provides the latest value of the secret `var_secret` as `VARIABLE_SECRET` e - `/app/secret/ver1` contains version referenced by `module.secret-manager.version_versions["credentials:v1"]` +Remember to grant access to secrets to the service account running Cloud Function. + ```hcl module "cf-http" { source = "./fabric/modules/cloud-function-v1" @@ -288,7 +295,22 @@ module "cf-http" { google_project_iam_member.bucket_default_compute_account_grant, ] } -# tftest fixtures=fixtures/secret-credentials.tf,fixtures/functions-default-sa-iam-grants.tf inventory=secrets.yaml e2e skip-tofu + +module "secret-manager" { + source = "./fabric/modules/secret-manager" + project_id = var.project_id + secrets = { + credentials = { + iam = { + "roles/secretmanager.secretAccessor" = [module.cf-http.service_account_iam_email] + } + versions = { + v1 = { data = "manual foo bar spam" } + } + } + } +} +# tftest fixtures=fixtures/functions-default-sa-iam-grants.tf inventory=secrets.yaml e2e skip-tofu ``` ### Using CMEK to encrypt function resources @@ -312,7 +334,7 @@ module "project" { ] iam = { # grant compute default service account that is used by Cloud Founction - # permission to read from the buckets so it can function sources + # permission to read from the buckets so it can read function sources "roles/storage.objectViewer" = [ "serviceAccount:${module.project.default_service_accounts.compute}" ] @@ -369,7 +391,7 @@ module "cf-http" { repository = module.artifact-registry.id } } -# tftest modules=4 resources=25 +# tftest inventory=cmek.yaml ``` ## Variables @@ -378,28 +400,28 @@ module "cf-http" { |---|---|:---:|:---:|:---:| | [bucket_name](variables.tf#L27) | Name of the bucket that will be used for the function code. It will be created with prefix prepended if bucket_config is not null. | string | ✓ | | | [bundle_config](variables.tf#L45) | Cloud function source. Path can point to a GCS object URI, or a local path. A local path to a zip archive will generate a GCS object using its basename, a folder will be zipped and the GCS object name inferred when not specified. | object({…}) | ✓ | | -| [name](variables.tf#L140) | Name used for cloud function and associated resources. | string | ✓ | | -| [project_id](variables.tf#L155) | Project id used for all resources. | string | ✓ | | -| [region](variables.tf#L160) | Region used for all resources. | string | ✓ | | +| [name](variables.tf#L158) | Name used for cloud function and associated resources. | string | ✓ | | +| [project_id](variables.tf#L173) | Project id used for all resources. | string | ✓ | | +| [region](variables.tf#L178) | Region used for all resources. | string | ✓ | | | [bucket_config](variables.tf#L17) | Enable and configure auto-created bucket. Set fields to null to use defaults. | object({…}) | | null | | [build_environment_variables](variables.tf#L33) | A set of key/value environment variable pairs available during build time. | map(string) | | {} | | [build_worker_pool](variables.tf#L39) | Build worker pool, in projects//locations//workerPools/ format. | string | | null | -| [description](variables.tf#L78) | Optional description. | string | | "Terraform managed." | -| [environment_variables](variables.tf#L84) | Cloud function environment variables. | map(string) | | {} | -| [function_config](variables.tf#L90) | Cloud function configuration. Defaults to using main as entrypoint, 1 instance with 256MiB of memory, and 180 second timeout. | object({…}) | | {…} | -| [https_security_level](variables.tf#L110) | The security level for the function: Allowed values are SECURE_ALWAYS, SECURE_OPTIONAL. | string | | null | -| [iam](variables.tf#L116) | IAM bindings for topic in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [ingress_settings](variables.tf#L122) | Control traffic that reaches the cloud function. Allowed values are ALLOW_ALL, ALLOW_INTERNAL_AND_GCLB and ALLOW_INTERNAL_ONLY . | string | | null | -| [kms_key](variables.tf#L128) | Resource name of a KMS crypto key (managed by the user) used to encrypt/decrypt function resources in key id format. If specified, you must also provide an artifact registry repository using the docker_repository field that was created with the same KMS crypto key. | string | | null | -| [labels](variables.tf#L134) | Resource labels. | map(string) | | {} | -| [prefix](variables.tf#L145) | Optional prefix used for resource names. | string | | null | -| [repository_settings](variables.tf#L165) | Docker Registry to use for storing the function's Docker images and specific repository. If kms_key is provided, the repository must have already been encrypted with the key. | object({…}) | | {…} | -| [secrets](variables.tf#L176) | Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format. | map(object({…})) | | {} | -| [service_account](variables.tf#L188) | Service account email. Unused if service account is auto-created. | string | | null | -| [service_account_create](variables.tf#L194) | Auto-create service account. | bool | | false | -| [trigger_config](variables.tf#L200) | Function trigger configuration. Leave null for HTTP trigger. | object({…}) | | null | -| [vpc_connector](variables.tf#L210) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | null | -| [vpc_connector_config](variables.tf#L220) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | +| [context](variables.tf#L78) | Context-specific interpolations. | object({…}) | | {} | +| [description](variables.tf#L96) | Optional description. | string | | "Terraform managed." | +| [environment_variables](variables.tf#L102) | Cloud function environment variables. | map(string) | | {} | +| [function_config](variables.tf#L108) | Cloud function configuration. Defaults to using main as entrypoint, 1 instance with 256MiB of memory, and 180 second timeout. | object({…}) | | {…} | +| [https_security_level](variables.tf#L128) | The security level for the function: Allowed values are SECURE_ALWAYS, SECURE_OPTIONAL. | string | | null | +| [iam](variables.tf#L134) | IAM bindings for topic in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [ingress_settings](variables.tf#L140) | Control traffic that reaches the cloud function. Allowed values are ALLOW_ALL, ALLOW_INTERNAL_AND_GCLB and ALLOW_INTERNAL_ONLY . | string | | null | +| [kms_key](variables.tf#L146) | Resource name of a KMS crypto key (managed by the user) used to encrypt/decrypt function resources in key id format. If specified, you must also provide an artifact registry repository using the docker_repository field that was created with the same KMS crypto key. | string | | null | +| [labels](variables.tf#L152) | Resource labels. | map(string) | | {} | +| [prefix](variables.tf#L163) | Optional prefix used for resource names. | string | | null | +| [repository_settings](variables.tf#L183) | Docker Registry to use for storing the function's Docker images and specific repository. If kms_key is provided, the repository must have already been encrypted with the key. | object({…}) | | {…} | +| [secrets](variables.tf#L194) | Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format. | map(object({…})) | | {} | +| [service_account_config](variables-serviceaccount.tf#L17) | Service account configurations. | object({…}) | | {} | +| [trigger_config](variables.tf#L206) | Function trigger configuration. Leave null for HTTP trigger. | object({…}) | | null | +| [vpc_connector](variables.tf#L216) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | {} | +| [vpc_connector_config](variables.tf#L227) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | ## Outputs @@ -421,5 +443,4 @@ module "cf-http" { - [cloudbuild-custom-pool.tf](../../tests/fixtures/cloudbuild-custom-pool.tf) - [functions-default-sa-iam-grants.tf](../../tests/fixtures/functions-default-sa-iam-grants.tf) - [pubsub.tf](../../tests/fixtures/pubsub.tf) -- [secret-credentials.tf](../../tests/fixtures/secret-credentials.tf) diff --git a/modules/cloud-function-v1/bundle.tf b/modules/cloud-function-v1/bundle.tf index 661ece842..97e9927e7 100644 --- a/modules/cloud-function-v1/bundle.tf +++ b/modules/cloud-function-v1/bundle.tf @@ -1,5 +1,5 @@ /** - * Copyright 2024 Google LLC + * 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. @@ -29,13 +29,13 @@ locals { resource "google_storage_bucket" "bucket" { count = var.bucket_config == null ? 0 : 1 - project = var.project_id + project = local.project_id name = "${local.prefix}${var.bucket_name}" uniform_bucket_level_access = true location = ( var.bucket_config.location == null - ? var.region - : var.bucket_config.location + ? local.location + : lookup(local.ctx.locations, var.bucket_config.location, var.bucket_config.location) ) labels = var.labels dynamic "lifecycle_rule" { @@ -66,7 +66,7 @@ data "archive_file" "bundle" { output_path = ( var.bundle_config.folder_options.archive_path != null ? pathexpand(var.bundle_config.folder_options.archive_path) - : "/tmp/bundle-${var.project_id}-${var.name}.zip" + : "/tmp/bundle-${local.project_id}-${var.name}.zip" ) output_file_mode = "0644" excludes = var.bundle_config.folder_options.excludes @@ -76,13 +76,20 @@ data "archive_file" "bundle" { resource "google_storage_bucket_object" "bundle" { count = local.bundle_type != "gcs" ? 1 : 0 - name = try( - "bundle-${data.archive_file.bundle[0].output_md5}.zip", - basename(var.bundle_config.path) + name = ( + local.bundle_type == "local-folder" + ? "bundle-${data.archive_file.bundle[0].output_md5}.zip" + : basename(var.bundle_config.path) ) bucket = local.bucket - source = try( - data.archive_file.bundle[0].output_path, - pathexpand(var.bundle_config.path) + source = ( + local.bundle_type == "local-folder" + ? data.archive_file.bundle[0].output_path + : pathexpand(var.bundle_config.path) + ) + source_md5hash = ( + local.bundle_type == "local-folder" + ? data.archive_file.bundle[0].output_md5 + : filemd5(pathexpand(var.bundle_config.path)) ) } diff --git a/modules/cloud-function-v1/main.tf b/modules/cloud-function-v1/main.tf index e51ded541..5d215226c 100644 --- a/modules/cloud-function-v1/main.tf +++ b/modules/cloud-function-v1/main.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * 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. @@ -15,6 +15,12 @@ */ locals { + _ctx_p = "$" + ctx = { + for k, v in var.context : k => { + for kk, vv in v : "${local._ctx_p}${k}:${kk}" => vv + } if k != "condition_vars" + } bucket = ( var.bucket_config == null ? var.bucket_name @@ -24,17 +30,14 @@ locals { : null ) ) - prefix = var.prefix == null ? "" : "${var.prefix}-" - service_account_email = ( - var.service_account_create - ? google_service_account.service_account[0].email - : var.service_account - ) + location = lookup(local.ctx.locations, var.region, var.region) + prefix = var.prefix == null ? "" : "${var.prefix}-" + project_id = lookup(local.ctx.project_ids, var.project_id, var.project_id) vpc_connector = ( - var.vpc_connector == null + var.vpc_connector.name == null ? null : ( - try(var.vpc_connector.create, false) == false + var.vpc_connector.create == false ? var.vpc_connector.name : google_vpc_access_connector.connector[0].id ) @@ -42,12 +45,17 @@ locals { } resource "google_vpc_access_connector" "connector" { - count = try(var.vpc_connector.create, false) == true ? 1 : 0 - project = var.project_id - name = var.vpc_connector.name - region = var.region - ip_cidr_range = var.vpc_connector_config.ip_cidr_range - network = var.vpc_connector_config.network + count = var.vpc_connector.create == true ? 1 : 0 + project = local.project_id + name = var.vpc_connector.name + region = local.location + ip_cidr_range = lookup(local.ctx.cidr_ranges, + var.vpc_connector_config.ip_cidr_range, + var.vpc_connector_config.ip_cidr_range + ) + network = lookup(local.ctx.networks, + var.vpc_connector_config.network, var.vpc_connector_config.network + ) max_instances = try(var.vpc_connector_config.instances.max, null) min_instances = try(var.vpc_connector_config.instances.min, null) max_throughput = try(var.vpc_connector_config.throughput.max, null) @@ -55,8 +63,8 @@ resource "google_vpc_access_connector" "connector" { } resource "google_cloudfunctions_function" "function" { - project = var.project_id - region = var.region + project = local.project_id + region = local.location name = "${local.prefix}${var.name}" description = var.description runtime = var.function_config.runtime @@ -78,7 +86,7 @@ resource "google_cloudfunctions_function" "function" { ingress_settings = var.ingress_settings build_worker_pool = var.build_worker_pool build_environment_variables = var.build_environment_variables - kms_key_name = var.kms_key + kms_key_name = var.kms_key == null ? null : lookup(local.ctx.kms_keys, var.kms_key, var.kms_key) docker_registry = try(var.repository_settings.registry, "ARTIFACT_REGISTRY") docker_repository = try(var.repository_settings.repository, null) vpc_connector = local.vpc_connector @@ -132,16 +140,12 @@ resource "google_cloudfunctions_function" "function" { resource "google_cloudfunctions_function_iam_binding" "default" { for_each = var.iam - project = var.project_id - region = var.region + project = local.project_id + region = local.location cloud_function = google_cloudfunctions_function.function.id role = each.key members = each.value -} - -resource "google_service_account" "service_account" { - count = var.service_account_create ? 1 : 0 - project = var.project_id - account_id = "tf-cf-${var.name}" - display_name = "Terraform Cloud Function ${var.name}." + lifecycle { + replace_triggered_by = [google_cloudfunctions_function.function] + } } diff --git a/modules/cloud-function-v1/serviceaccount.tf b/modules/cloud-function-v1/serviceaccount.tf new file mode 100644 index 000000000..f1fdec866 --- /dev/null +++ b/modules/cloud-function-v1/serviceaccount.tf @@ -0,0 +1,53 @@ +/** + * 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 { + service_account_email = ( + var.service_account_config.create + ? google_service_account.service_account[0].email + : lookup( + local.ctx.iam_principals, + var.service_account_config.email, + var.service_account_config.email + ) + ) + service_account_roles = [ + for role in var.service_account_config.roles + : lookup(local.ctx.custom_roles, role, role) + ] +} + +resource "google_service_account" "service_account" { + count = var.service_account_config.create ? 1 : 0 + project = local.project_id + account_id = coalesce(var.service_account_config.name, var.name) + display_name = coalesce( + var.service_account_config.display_name, + var.service_account_config.name, + var.name + ) +} + +resource "google_project_iam_member" "default" { + for_each = ( + var.service_account_config.create + ? toset(local.service_account_roles) + : toset([]) + ) + role = each.key + project = local.project_id + member = google_service_account.service_account[0].member +} diff --git a/modules/cloud-function-v1/variables-serviceaccount.tf b/modules/cloud-function-v1/variables-serviceaccount.tf new file mode 100644 index 000000000..878feed9f --- /dev/null +++ b/modules/cloud-function-v1/variables-serviceaccount.tf @@ -0,0 +1,31 @@ +/** + * 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. + */ + +variable "service_account_config" { + description = "Service account configurations." + type = object({ + create = optional(bool, true) + display_name = optional(string) + email = optional(string) + name = optional(string) + roles = optional(list(string), [ + "roles/logging.logWriter", + "roles/monitoring.metricWriter" + ]) + }) + nullable = false + default = {} +} diff --git a/modules/cloud-function-v1/variables.tf b/modules/cloud-function-v1/variables.tf index 98f1f10af..12843bc59 100644 --- a/modules/cloud-function-v1/variables.tf +++ b/modules/cloud-function-v1/variables.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * 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. @@ -75,6 +75,24 @@ variable "bundle_config" { } } +variable "context" { + description = "Context-specific interpolations." + type = object({ + condition_vars = optional(map(map(string)), {}) # not needed here? + cidr_ranges = optional(map(string), {}) + custom_roles = optional(map(string), {}) + iam_principals = optional(map(string), {}) + kms_keys = optional(map(string), {}) + locations = optional(map(string), {}) + networks = optional(map(string), {}) + project_ids = optional(map(string), {}) + subnets = optional(map(string), {}) + tag_values = optional(map(string), {}) # not needed here? + }) + nullable = false + default = {} +} + variable "description" { description = "Optional description." type = string @@ -185,18 +203,6 @@ variable "secrets" { default = {} } -variable "service_account" { - description = "Service account email. Unused if service account is auto-created." - type = string - default = null -} - -variable "service_account_create" { - description = "Auto-create service account." - type = bool - default = false -} - variable "trigger_config" { description = "Function trigger configuration. Leave null for HTTP trigger." type = object({ @@ -210,11 +216,12 @@ variable "trigger_config" { variable "vpc_connector" { description = "VPC connector configuration. Set create to 'true' if a new connector needs to be created." type = object({ - create = bool - name = string - egress_settings = string + create = optional(bool, false) + name = optional(string) + egress_settings = optional(string) }) - default = null + nullable = false + default = {} } variable "vpc_connector_config" { diff --git a/tests/modules/cloud_function_v1/examples/bucket-creation.yaml b/tests/modules/cloud_function_v1/examples/bucket-creation.yaml index 964682337..8d8b8bda2 100644 --- a/tests/modules/cloud_function_v1/examples/bucket-creation.yaml +++ b/tests/modules/cloud_function_v1/examples/bucket-creation.yaml @@ -27,10 +27,9 @@ values: name: test-bucket counts: + archive_file: 1 google_cloudfunctions_function: 1 google_storage_bucket: 1 google_storage_bucket_object: 1 - modules: 1 - resources: 6 outputs: {} diff --git a/tests/modules/cloud_function_v1/examples/cmek.yaml b/tests/modules/cloud_function_v1/examples/cmek.yaml new file mode 100644 index 000000000..cc697041e --- /dev/null +++ b/tests/modules/cloud_function_v1/examples/cmek.yaml @@ -0,0 +1,73 @@ +# 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. + + +values: + module.artifact-registry.google_artifact_registry_repository.registry: + cleanup_policies: [] + cleanup_policy_dry_run: null + description: Terraform-managed registry + docker_config: [] + effective_labels: + goog-terraform-provisioned: 'true' + format: DOCKER + labels: null + location: europe-west9 + maven_config: [] + mode: STANDARD_REPOSITORY + project: test-cf-v1 + remote_repository_config: [] + repository_id: registry + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + virtual_repository_config: [] + vulnerability_scanning_config: + - enablement_config: null + module.artifact-registry.google_artifact_registry_repository_iam_binding.authoritative["roles/artifactregistry.createOnPushWriter"]: + condition: [] + location: europe-west9 + project: test-cf-v1 + role: roles/artifactregistry.createOnPushWriter + module.kms.google_kms_crypto_key.default["key-regional"]: + effective_labels: + goog-terraform-provisioned: 'true' + labels: null + name: key-regional + purpose: ENCRYPT_DECRYPT + rotation_period: null + skip_initial_version_creation: false + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + module.kms.google_kms_key_ring.default[0]: + location: europe-west9 + name: test-keyring + project: test-cf-v1 + timeouts: null + module.kms.google_kms_key_ring_iam_binding.authoritative["roles/cloudkms.cryptoKeyEncrypterDecrypter"]: + condition: [] + role: roles/cloudkms.cryptoKeyEncrypterDecrypter + module.project.google_project_iam_binding.authoritative["roles/storage.objectViewer"]: + condition: [] + project: test-cf-v1 + role: roles/storage.objectViewer + +counts: + google_artifact_registry_repository: 1 + google_artifact_registry_repository_iam_binding: 1 + google_cloudfunctions_function: 1 + google_kms_crypto_key: 1 + google_kms_key_ring: 1 + google_kms_key_ring_iam_binding: 1 diff --git a/tests/modules/cloud_function_v1/examples/custom-bundle.yaml b/tests/modules/cloud_function_v1/examples/custom-bundle.yaml new file mode 100644 index 000000000..ff8866600 --- /dev/null +++ b/tests/modules/cloud_function_v1/examples/custom-bundle.yaml @@ -0,0 +1,113 @@ +# 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. + +values: + google_project_iam_member.artifact_writer: + condition: [] + member: serviceAccount:123-compute@developer.gserviceaccount.com + project: project-id + role: roles/artifactregistry.createOnPushWriter + google_project_iam_member.bucket_default_compute_account_grant: + condition: [] + member: serviceAccount:123-compute@developer.gserviceaccount.com + project: project-id + role: roles/storage.objectViewer + module.cf-http.data.archive_file.bundle[0]: + exclude_symlink_directories: null + excludes: + - __pycache__ + output_file_mode: '0644' + output_path: bundle.zip + source: [] + source_content: null + source_content_filename: null + source_dir: assets/sample-function/ + source_file: null + type: zip + module.cf-http.google_cloudfunctions_function.function: + available_memory_mb: 256 + build_environment_variables: null + build_worker_pool: null + description: Terraform managed. + docker_registry: ARTIFACT_REGISTRY + docker_repository: null + effective_labels: + goog-terraform-provisioned: 'true' + entry_point: main + environment_variables: null + https_trigger_security_level: SECURE_ALWAYS + ingress_settings: ALLOW_ALL + kms_key_name: null + labels: null + max_instances: 1 + min_instances: null + name: test-cf-http + on_deploy_update_policy: [] + project: project-id + region: europe-west9 + runtime: python310 + secret_environment_variables: [] + secret_volumes: [] + service_account_email: test-cf-http@project-id.iam.gserviceaccount.com + source_archive_bucket: bucket + source_repository: [] + terraform_labels: + goog-terraform-provisioned: 'true' + timeout: 180 + timeouts: null + trigger_http: true + vpc_connector: null + module.cf-http.google_project_iam_member.default["roles/logging.logWriter"]: + condition: [] + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + role: roles/logging.logWriter + module.cf-http.google_project_iam_member.default["roles/monitoring.metricWriter"]: + condition: [] + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + role: roles/monitoring.metricWriter + module.cf-http.google_service_account.service_account[0]: + account_id: test-cf-http + create_ignore_already_exists: null + description: null + disabled: false + display_name: test-cf-http + email: test-cf-http@project-id.iam.gserviceaccount.com + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + timeouts: null + module.cf-http.google_storage_bucket_object.bundle[0]: + bucket: bucket + cache_control: null + content_disposition: null + content_encoding: null + content_language: null + customer_encryption: [] + deletion_policy: null + detect_md5hash: null + event_based_hold: null + force_empty_content_type: null + metadata: null + retention: [] + source: bundle.zip + temporary_hold: null + timeouts: null + +counts: + archive_file: 1 + google_cloudfunctions_function: 1 + google_project_iam_member: 4 + google_service_account: 1 + google_storage_bucket_object: 1 diff --git a/tests/modules/cloud_function_v1/examples/http-trigger.yaml b/tests/modules/cloud_function_v1/examples/http-trigger.yaml new file mode 100644 index 000000000..ce1d501a1 --- /dev/null +++ b/tests/modules/cloud_function_v1/examples/http-trigger.yaml @@ -0,0 +1,115 @@ +# 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. + +values: + google_project_iam_member.artifact_writer: + condition: [] + member: serviceAccount:123-compute@developer.gserviceaccount.com + project: project-id + role: roles/artifactregistry.createOnPushWriter + google_project_iam_member.bucket_default_compute_account_grant: + condition: [] + member: serviceAccount:123-compute@developer.gserviceaccount.com + project: project-id + role: roles/storage.objectViewer + module.cf-http.data.archive_file.bundle[0]: + exclude_symlink_directories: null + excludes: null + output_file_mode: '0644' + output_path: /tmp/bundle-project-id-test-cf-http.zip + source: [] + source_content: null + source_content_filename: null + source_dir: assets/sample-function/ + source_file: null + type: zip + module.cf-http.google_cloudfunctions_function.function: + available_memory_mb: 256 + build_environment_variables: null + build_worker_pool: null + description: Terraform managed. + docker_registry: ARTIFACT_REGISTRY + docker_repository: null + effective_labels: + goog-terraform-provisioned: 'true' + entry_point: main + environment_variables: null + https_trigger_security_level: SECURE_ALWAYS + ingress_settings: ALLOW_ALL + kms_key_name: null + labels: null + max_instances: 1 + min_instances: null + name: test-cf-http + on_deploy_update_policy: [] + project: project-id + region: europe-west9 + runtime: python310 + secret_environment_variables: [] + secret_volumes: [] + service_account_email: test-cf-http@project-id.iam.gserviceaccount.com + source_archive_bucket: bucket + source_repository: [] + terraform_labels: + goog-terraform-provisioned: 'true' + timeout: 180 + timeouts: null + trigger_http: true + vpc_connector: null + module.cf-http.google_project_iam_member.default["roles/logging.logWriter"]: + condition: [] + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + role: roles/logging.logWriter + module.cf-http.google_project_iam_member.default["roles/monitoring.metricWriter"]: + condition: [] + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + role: roles/monitoring.metricWriter + module.cf-http.google_service_account.service_account[0]: + account_id: test-cf-http + create_ignore_already_exists: null + description: null + disabled: false + display_name: test-cf-http + email: test-cf-http@project-id.iam.gserviceaccount.com + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + timeouts: null + module.cf-http.google_storage_bucket_object.bundle[0]: + bucket: bucket + cache_control: null + content_disposition: null + content_encoding: null + content_language: null + customer_encryption: [] + deletion_policy: null + detect_md5hash: null + event_based_hold: null + force_empty_content_type: null + metadata: null + retention: [] + source: /tmp/bundle-project-id-test-cf-http.zip + temporary_hold: null + timeouts: null + +counts: + archive_file: 1 + google_cloudfunctions_function: 1 + google_project_iam_member: 4 + google_service_account: 1 + google_storage_bucket_object: 1 + + +outputs: {} diff --git a/tests/modules/cloud_function_v1/examples/iam.yaml b/tests/modules/cloud_function_v1/examples/iam.yaml index 8473e6f3d..31e5b30cc 100644 --- a/tests/modules/cloud_function_v1/examples/iam.yaml +++ b/tests/modules/cloud_function_v1/examples/iam.yaml @@ -22,7 +22,9 @@ values: role: roles/cloudfunctions.invoker counts: + archive_file: 1 google_cloudfunctions_function: 1 + google_cloudfunctions_function_iam_binding: 1 + google_project_iam_member: 4 + google_service_account: 1 google_storage_bucket_object: 1 - modules: 1 - resources: 6 diff --git a/tests/modules/cloud_function_v1/examples/multiple_functions.yaml b/tests/modules/cloud_function_v1/examples/multiple_functions.yaml index f550b2c75..2cc3fecb1 100644 --- a/tests/modules/cloud_function_v1/examples/multiple_functions.yaml +++ b/tests/modules/cloud_function_v1/examples/multiple_functions.yaml @@ -19,7 +19,8 @@ values: source: /tmp/bundle-project-id-test-cf-http-two.zip counts: + archive_file: 1 google_cloudfunctions_function: 2 + google_project_iam_member: 6 + google_service_account: 2 google_storage_bucket_object: 2 - modules: 2 - resources: 7 diff --git a/tests/modules/cloud_function_v1/examples/private-build-pool.yaml b/tests/modules/cloud_function_v1/examples/private-build-pool.yaml new file mode 100644 index 000000000..b8db1e2c7 --- /dev/null +++ b/tests/modules/cloud_function_v1/examples/private-build-pool.yaml @@ -0,0 +1,125 @@ +# 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. + +values: + google_cloudbuild_worker_pool.pool: + annotations: null + display_name: null + location: europe-west9 + name: custom-pool + network_config: [] + private_service_connect: [] + project: project-id + timeouts: null + worker_config: + - disk_size_gb: 100 + machine_type: e2-standard-4 + no_external_ip: false + google_project_iam_member.artifact_writer: + condition: [] + member: serviceAccount:123-compute@developer.gserviceaccount.com + project: project-id + role: roles/artifactregistry.createOnPushWriter + google_project_iam_member.bucket_default_compute_account_grant: + condition: [] + member: serviceAccount:123-compute@developer.gserviceaccount.com + project: project-id + role: roles/storage.objectViewer + module.cf-http.data.archive_file.bundle[0]: + exclude_symlink_directories: null + excludes: null + output_file_mode: '0644' + output_path: /tmp/bundle-project-id-test-cf-http.zip + source: [] + source_content: null + source_content_filename: null + source_dir: assets/sample-function/ + source_file: null + type: zip + module.cf-http.google_cloudfunctions_function.function: + available_memory_mb: 256 + build_environment_variables: null + description: Terraform managed. + docker_registry: ARTIFACT_REGISTRY + docker_repository: null + effective_labels: + goog-terraform-provisioned: 'true' + entry_point: main + environment_variables: null + https_trigger_security_level: SECURE_ALWAYS + ingress_settings: ALLOW_ALL + kms_key_name: null + labels: null + max_instances: 1 + min_instances: null + name: test-cf-http + on_deploy_update_policy: [] + project: project-id + region: europe-west9 + runtime: python310 + secret_environment_variables: [] + secret_volumes: [] + service_account_email: test-cf-http@project-id.iam.gserviceaccount.com + source_archive_bucket: bucket + source_repository: [] + terraform_labels: + goog-terraform-provisioned: 'true' + timeout: 180 + timeouts: null + trigger_http: true + vpc_connector: null + module.cf-http.google_project_iam_member.default["roles/logging.logWriter"]: + condition: [] + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + role: roles/logging.logWriter + module.cf-http.google_project_iam_member.default["roles/monitoring.metricWriter"]: + condition: [] + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + role: roles/monitoring.metricWriter + module.cf-http.google_service_account.service_account[0]: + account_id: test-cf-http + create_ignore_already_exists: null + description: null + disabled: false + display_name: test-cf-http + email: test-cf-http@project-id.iam.gserviceaccount.com + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + timeouts: null + module.cf-http.google_storage_bucket_object.bundle[0]: + bucket: bucket + cache_control: null + content_disposition: null + content_encoding: null + content_language: null + customer_encryption: [] + deletion_policy: null + detect_md5hash: null + event_based_hold: null + force_empty_content_type: null + metadata: null + retention: [] + source: /tmp/bundle-project-id-test-cf-http.zip + temporary_hold: null + timeouts: null + +counts: + archive_file: 1 + google_cloudbuild_worker_pool: 1 + google_cloudfunctions_function: 1 + google_project_iam_member: 4 + google_service_account: 1 + google_storage_bucket_object: 1 diff --git a/tests/modules/cloud_function_v1/examples/pubsub-non-http-trigger.yaml b/tests/modules/cloud_function_v1/examples/pubsub-non-http-trigger.yaml new file mode 100644 index 000000000..4e3b1c777 --- /dev/null +++ b/tests/modules/cloud_function_v1/examples/pubsub-non-http-trigger.yaml @@ -0,0 +1,84 @@ +# 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. + +values: + module.cf-http.google_cloudfunctions_function.function: + available_memory_mb: 256 + build_environment_variables: null + build_worker_pool: null + description: Terraform managed. + docker_registry: ARTIFACT_REGISTRY + docker_repository: null + effective_labels: + goog-terraform-provisioned: 'true' + entry_point: main + environment_variables: null + event_trigger: + - event_type: google.pubsub.topic.publish + resource: topic + https_trigger_security_level: SECURE_ALWAYS + ingress_settings: ALLOW_ALL + kms_key_name: null + labels: null + max_instances: 1 + min_instances: null + name: test-cf-http + on_deploy_update_policy: [] + project: project-id + region: europe-west9 + runtime: python310 + secret_environment_variables: [] + secret_volumes: [] + service_account_email: test-cf-http@project-id.iam.gserviceaccount.com + source_archive_bucket: bucket + source_repository: [] + terraform_labels: + goog-terraform-provisioned: 'true' + timeout: 180 + timeouts: null + trigger_http: null + vpc_connector: null + module.pubsub.google_pubsub_topic.default: + effective_labels: + goog-terraform-provisioned: 'true' + ingestion_data_source_settings: [] + kms_key_name: null + labels: null + message_retention_duration: null + message_transforms: [] + name: topic + project: project-id + schema_settings: [] + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + module.pubsub.google_pubsub_topic_iam_binding.authoritative["roles/pubsub.subscriber"]: + condition: [] + members: + - serviceAccount:123-compute@developer.gserviceaccount.com + project: project-id + role: roles/pubsub.subscriber + topic: topic + +counts: + archive_file: 1 + google_cloudfunctions_function: 1 + google_project_iam_member: 4 + google_pubsub_topic: 1 + google_pubsub_topic_iam_binding: 1 + google_service_account: 1 + google_storage_bucket_object: 1 + + +outputs: {} diff --git a/tests/modules/cloud_function_v1/examples/secrets.yaml b/tests/modules/cloud_function_v1/examples/secrets.yaml index dc5d432bd..01cfaa646 100644 --- a/tests/modules/cloud_function_v1/examples/secrets.yaml +++ b/tests/modules/cloud_function_v1/examples/secrets.yaml @@ -30,5 +30,3 @@ values: counts: google_cloudfunctions_function: 1 google_storage_bucket_object: 1 - modules: 2 - resources: 8 diff --git a/tests/modules/cloud_function_v1/examples/service-account.yaml b/tests/modules/cloud_function_v1/examples/service-account.yaml new file mode 100644 index 000000000..767d86a40 --- /dev/null +++ b/tests/modules/cloud_function_v1/examples/service-account.yaml @@ -0,0 +1,75 @@ +# 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. + +values: + module.cf-http.google_cloudfunctions_function.function: + available_memory_mb: 256 + build_environment_variables: null + build_worker_pool: null + description: Terraform managed. + docker_registry: ARTIFACT_REGISTRY + docker_repository: null + effective_labels: + goog-terraform-provisioned: 'true' + entry_point: main + environment_variables: null + https_trigger_security_level: SECURE_ALWAYS + ingress_settings: ALLOW_ALL + kms_key_name: null + labels: null + max_instances: 1 + min_instances: null + name: test-cf-http + on_deploy_update_policy: [] + project: project-id + region: europe-west9 + runtime: python310 + secret_environment_variables: [] + secret_volumes: [] + service_account_email: test-cf-http@project-id.iam.gserviceaccount.com + source_archive_bucket: bucket + source_repository: [] + terraform_labels: + goog-terraform-provisioned: 'true' + timeout: 180 + timeouts: null + trigger_http: true + vpc_connector: null + module.cf-http.google_project_iam_member.default["roles/logging.logWriter"]: + condition: [] + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + role: roles/logging.logWriter + module.cf-http.google_project_iam_member.default["roles/monitoring.metricWriter"]: + condition: [] + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + role: roles/monitoring.metricWriter + module.cf-http.google_service_account.service_account[0]: + account_id: test-cf-http + create_ignore_already_exists: null + description: null + disabled: false + display_name: test-cf-http + email: test-cf-http@project-id.iam.gserviceaccount.com + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + timeouts: null + +counts: + archive_file: 1 + google_cloudfunctions_function: 1 + google_project_iam_member: 4 + google_service_account: 1 + google_storage_bucket_object: 1 diff --git a/tests/modules/cloud_function_v1/kms.tfvars b/tests/modules/cloud_function_v1/kms.tfvars new file mode 100644 index 000000000..81f3b5190 --- /dev/null +++ b/tests/modules/cloud_function_v1/kms.tfvars @@ -0,0 +1,11 @@ +project_id = "project" +region = "region" +name = "test-cf-kms" +bucket_name = "bucket" +bundle_config = { + path = "gs://assets/sample-function.zip" +} +kms_key = "kms_key_id" +repository_settings = { + repository = "artifact_registry_id" +} diff --git a/tests/modules/cloud_function_v1/kms.yaml b/tests/modules/cloud_function_v1/kms.yaml new file mode 100644 index 000000000..813f42f7c --- /dev/null +++ b/tests/modules/cloud_function_v1/kms.yaml @@ -0,0 +1,39 @@ +# 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. + +values: + google_cloudfunctions_function.function: + docker_repository: artifact_registry_id + kms_key_name: kms_key_id + name: test-cf-kms + project: project + region: region + service_account_email: test-cf-kms@project.iam.gserviceaccount.com + source_archive_bucket: bucket + source_archive_object: sample-function.zip + +counts: + google_cloudfunctions_function: 1 + +outputs: + bucket: null + bucket_name: bucket + function: __missing__ + function_name: test-cf-kms + id: __missing__ + invoke_command: __missing__ + service_account: __missing__ + service_account_email: test-cf-kms@project.iam.gserviceaccount.com + service_account_iam_email: serviceAccount:test-cf-kms@project.iam.gserviceaccount.com + vpc_connector: null diff --git a/tests/modules/cloud_function_v1/tftest.yaml b/tests/modules/cloud_function_v1/tftest.yaml new file mode 100644 index 000000000..448e96615 --- /dev/null +++ b/tests/modules/cloud_function_v1/tftest.yaml @@ -0,0 +1,17 @@ +# 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. + +module: modules/cloud-function-v1 +tests: + kms: From 36f2e654655899fd05f930bcc29e3d05c6669fc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Mon, 20 Oct 2025 11:10:01 +0000 Subject: [PATCH 4/9] Cloud Function v2 - contexts and interface refactor --- modules/cloud-function-v2/README.md | 76 ++++++---- modules/cloud-function-v2/bundle.tf | 29 ++-- modules/cloud-function-v2/main.tf | 47 +++---- modules/cloud-function-v2/serviceaccount.tf | 53 +++++++ .../variables-serviceaccount.tf | 31 +++++ modules/cloud-function-v2/variables.tf | 30 ++-- .../examples/bucket-creation.yaml | 3 +- .../examples/custom-bundle.yaml | 118 ++++++++++++++++ .../examples/http-trigger.yaml | 120 ++++++++++++++++ .../cloud_function_v2/examples/iam.yaml | 3 - .../examples/multiple_functions.yaml | 2 +- .../examples/private-build-pool.yaml | 130 ++++++++++++++++++ .../examples/pubsub-non-http-trigger.yaml | 91 ++++++++++++ .../cloud_function_v2/examples/secrets.yaml | 54 +++++++- .../examples/service-account-1.yaml | 80 +++++++++++ .../examples/service-account-2.yaml | 58 ++++++++ tools/duplicate-diff.py | 33 ++++- 17 files changed, 868 insertions(+), 90 deletions(-) create mode 100644 modules/cloud-function-v2/serviceaccount.tf create mode 100644 modules/cloud-function-v2/variables-serviceaccount.tf create mode 100644 tests/modules/cloud_function_v2/examples/custom-bundle.yaml create mode 100644 tests/modules/cloud_function_v2/examples/http-trigger.yaml create mode 100644 tests/modules/cloud_function_v2/examples/private-build-pool.yaml create mode 100644 tests/modules/cloud_function_v2/examples/pubsub-non-http-trigger.yaml create mode 100644 tests/modules/cloud_function_v2/examples/service-account-1.yaml create mode 100644 tests/modules/cloud_function_v2/examples/service-account-2.yaml diff --git a/modules/cloud-function-v2/README.md b/modules/cloud-function-v2/README.md index 0c0fa2c44..5617eaad0 100644 --- a/modules/cloud-function-v2/README.md +++ b/modules/cloud-function-v2/README.md @@ -43,7 +43,7 @@ module "cf-http" { google_project_iam_member.bucket_default_compute_account_grant, ] } -# tftest modules=1 resources=5 fixtures=fixtures/functions-default-sa-iam-grants.tf e2e +# tftest inventory=http-trigger.yaml fixtures=fixtures/functions-default-sa-iam-grants.tf e2e ``` ### PubSub and non-HTTP triggers @@ -80,7 +80,7 @@ module "cf-http" { google_project_iam_member.bucket_default_compute_account_grant, ] } -# tftest modules=3 resources=9 fixtures=fixtures/pubsub.tf,fixtures/functions-default-sa-iam-grants.tf e2e +# tftest inventory=pubsub-non-http-trigger.yaml fixtures=fixtures/pubsub.tf,fixtures/functions-default-sa-iam-grants.tf e2e ``` Ensure that pubsub service identity (`service-[project number]@gcp-sa-pubsub.iam.gserviceaccount.com` has `roles/iam.serviceAccountTokenCreator` @@ -150,12 +150,14 @@ module "cf-http" { bundle_config = { path = "assets/sample-function/" } - service_account_create = true + service_account_config = { + create = true + } depends_on = [ google_project_iam_member.bucket_default_compute_account_grant, ] } -# tftest modules=1 resources=6 fixtures=fixtures/functions-default-sa-iam-grants.tf e2e +# tftest inventory=service-account-1.yaml fixtures=fixtures/functions-default-sa-iam-grants.tf e2e ``` To use an externally managed service account, pass its email in `service_account` and leave `service_account_create` to `false` (the default). @@ -170,12 +172,15 @@ module "cf-http" { bundle_config = { path = "assets/sample-function/" } - service_account = var.service_account.email + service_account_config = { + create = false + email = var.service_account.email + } depends_on = [ google_project_iam_member.bucket_default_compute_account_grant, ] } -# tftest modules=1 resources=5 fixtures=fixtures/functions-default-sa-iam-grants.tf e2e +# tftest inventory=service-account-2.yaml fixtures=fixtures/functions-default-sa-iam-grants.tf e2e ``` ### Custom bundle config @@ -206,7 +211,7 @@ module "cf-http" { google_project_iam_member.bucket_default_compute_account_grant, ] } -# tftest modules=1 resources=5 fixtures=fixtures/functions-default-sa-iam-grants.tf e2e +# tftest inventory=custom-bundle.yaml fixtures=fixtures/functions-default-sa-iam-grants.tf e2e ``` ### Private Cloud Build Pool @@ -228,7 +233,7 @@ module "cf-http" { google_project_iam_member.bucket_default_compute_account_grant, ] } -# tftest modules=1 resources=6 fixtures=fixtures/functions-default-sa-iam-grants.tf,fixtures/cloudbuild-custom-pool.tf e2e +# tftest inventory=private-build-pool.yaml fixtures=fixtures/functions-default-sa-iam-grants.tf,fixtures/cloudbuild-custom-pool.tf e2e ``` ### Multiple Cloud Functions within project @@ -269,6 +274,8 @@ This provides the latest value of the secret `var_secret` as `VARIABLE_SECRET` e - `/app/secret/ver1` contains version referenced by `module.secret-manager.version_versions["credentials:v1"]` +Remember to grant access to secrets to the service account running Cloud Function. + ```hcl module "cf-http" { source = "./fabric/modules/cloud-function-v2" @@ -302,7 +309,21 @@ module "cf-http" { ] } -# tftest fixtures=fixtures/secret-credentials.tf,fixtures/functions-default-sa-iam-grants.tf inventory=secrets.yaml e2e skip-tofu +module "secret-manager" { + source = "./fabric/modules/secret-manager" + project_id = var.project_id + secrets = { + credentials = { + iam = { + "roles/secretmanager.secretAccessor" = [module.cf-http.service_account_iam_email] + } + versions = { + v1 = { data = "manual foo bar spam" } + } + } + } +} +# tftest fixtures=fixtures/functions-default-sa-iam-grants.tf inventory=secrets.yaml e2e skip-tofu ``` ## Variables @@ -311,28 +332,28 @@ module "cf-http" { |---|---|:---:|:---:|:---:| | [bucket_name](variables.tf#L27) | Name of the bucket that will be used for the function code. It will be created with prefix prepended if bucket_config is not null. | string | ✓ | | | [bundle_config](variables.tf#L51) | Cloud function source. Path can point to a GCS object URI, or a local path. A local path to a zip archive will generate a GCS object using its basename, a folder will be zipped and the GCS object name inferred when not specified. | object({…}) | ✓ | | -| [name](variables.tf#L149) | Name used for cloud function and associated resources. | string | ✓ | | -| [project_id](variables.tf#L164) | Project id used for all resources. | string | ✓ | | -| [region](variables.tf#L169) | Region used for all resources. | string | ✓ | | +| [name](variables.tf#L167) | Name used for cloud function and associated resources. | string | ✓ | | +| [project_id](variables.tf#L182) | Project id used for all resources. | string | ✓ | | +| [region](variables.tf#L187) | Region used for all resources. | string | ✓ | | | [bucket_config](variables.tf#L17) | Enable and configure auto-created bucket. Set fields to null to use defaults. | object({…}) | | null | | [build_environment_variables](variables.tf#L33) | A set of key/value environment variable pairs available during build time. | map(string) | | {} | | [build_service_account](variables.tf#L39) | Build service account email. | string | | null | | [build_worker_pool](variables.tf#L45) | Build worker pool, in projects//locations//workerPools/ format. | string | | null | -| [description](variables.tf#L84) | Optional description. | string | | "Terraform managed." | -| [docker_repository_id](variables.tf#L90) | User managed repository created in Artifact Registry. | string | | null | -| [environment_variables](variables.tf#L96) | Cloud function environment variables. | map(string) | | {…} | -| [function_config](variables.tf#L104) | Cloud function configuration. Defaults to using main as entrypoint, 1 instance with 256MiB of memory, and 180 second timeout. | object({…}) | | {…} | -| [iam](variables.tf#L125) | IAM bindings for topic in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [ingress_settings](variables.tf#L131) | Control traffic that reaches the cloud function. Allowed values are ALLOW_ALL, ALLOW_INTERNAL_AND_GCLB and ALLOW_INTERNAL_ONLY . | string | | null | -| [kms_key](variables.tf#L137) | Resource name of a KMS crypto key (managed by the user) used to encrypt/decrypt function resources in key id format. If specified, you must also provide an artifact registry repository using the docker_repository_id field that was created with the same KMS crypto key. | string | | null | -| [labels](variables.tf#L143) | Resource labels. | map(string) | | {} | -| [prefix](variables.tf#L154) | Optional prefix used for resource names. | string | | null | -| [secrets](variables.tf#L174) | Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format. | map(object({…})) | | {} | -| [service_account](variables.tf#L186) | Service account email. Unused if service account is auto-created. | string | | null | -| [service_account_create](variables.tf#L192) | Auto-create service account. | bool | | false | -| [trigger_config](variables.tf#L198) | Function trigger configuration. Leave null for HTTP trigger. | object({…}) | | null | -| [vpc_connector](variables.tf#L216) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | {} | -| [vpc_connector_config](variables.tf#L227) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | +| [context](variables.tf#L84) | Context-specific interpolations. | object({…}) | | {} | +| [description](variables.tf#L102) | Optional description. | string | | "Terraform managed." | +| [docker_repository_id](variables.tf#L108) | User managed repository created in Artifact Registry. | string | | null | +| [environment_variables](variables.tf#L114) | Cloud function environment variables. | map(string) | | {…} | +| [function_config](variables.tf#L122) | Cloud function configuration. Defaults to using main as entrypoint, 1 instance with 256MiB of memory, and 180 second timeout. | object({…}) | | {…} | +| [iam](variables.tf#L143) | IAM bindings for topic in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [ingress_settings](variables.tf#L149) | Control traffic that reaches the cloud function. Allowed values are ALLOW_ALL, ALLOW_INTERNAL_AND_GCLB and ALLOW_INTERNAL_ONLY . | string | | null | +| [kms_key](variables.tf#L155) | Resource name of a KMS crypto key (managed by the user) used to encrypt/decrypt function resources in key id format. If specified, you must also provide an artifact registry repository using the docker_repository_id field that was created with the same KMS crypto key. | string | | null | +| [labels](variables.tf#L161) | Resource labels. | map(string) | | {} | +| [prefix](variables.tf#L172) | Optional prefix used for resource names. | string | | null | +| [secrets](variables.tf#L192) | Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format. | map(object({…})) | | {} | +| [service_account_config](variables-serviceaccount.tf#L17) | Service account configurations. | object({…}) | | {} | +| [trigger_config](variables.tf#L204) | Function trigger configuration. Leave null for HTTP trigger. | object({…}) | | null | +| [vpc_connector](variables.tf#L222) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | {} | +| [vpc_connector_config](variables.tf#L233) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | ## Outputs @@ -358,5 +379,4 @@ module "cf-http" { - [cloudbuild-custom-pool.tf](../../tests/fixtures/cloudbuild-custom-pool.tf) - [functions-default-sa-iam-grants.tf](../../tests/fixtures/functions-default-sa-iam-grants.tf) - [pubsub.tf](../../tests/fixtures/pubsub.tf) -- [secret-credentials.tf](../../tests/fixtures/secret-credentials.tf) diff --git a/modules/cloud-function-v2/bundle.tf b/modules/cloud-function-v2/bundle.tf index 661ece842..97e9927e7 100644 --- a/modules/cloud-function-v2/bundle.tf +++ b/modules/cloud-function-v2/bundle.tf @@ -1,5 +1,5 @@ /** - * Copyright 2024 Google LLC + * 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. @@ -29,13 +29,13 @@ locals { resource "google_storage_bucket" "bucket" { count = var.bucket_config == null ? 0 : 1 - project = var.project_id + project = local.project_id name = "${local.prefix}${var.bucket_name}" uniform_bucket_level_access = true location = ( var.bucket_config.location == null - ? var.region - : var.bucket_config.location + ? local.location + : lookup(local.ctx.locations, var.bucket_config.location, var.bucket_config.location) ) labels = var.labels dynamic "lifecycle_rule" { @@ -66,7 +66,7 @@ data "archive_file" "bundle" { output_path = ( var.bundle_config.folder_options.archive_path != null ? pathexpand(var.bundle_config.folder_options.archive_path) - : "/tmp/bundle-${var.project_id}-${var.name}.zip" + : "/tmp/bundle-${local.project_id}-${var.name}.zip" ) output_file_mode = "0644" excludes = var.bundle_config.folder_options.excludes @@ -76,13 +76,20 @@ data "archive_file" "bundle" { resource "google_storage_bucket_object" "bundle" { count = local.bundle_type != "gcs" ? 1 : 0 - name = try( - "bundle-${data.archive_file.bundle[0].output_md5}.zip", - basename(var.bundle_config.path) + name = ( + local.bundle_type == "local-folder" + ? "bundle-${data.archive_file.bundle[0].output_md5}.zip" + : basename(var.bundle_config.path) ) bucket = local.bucket - source = try( - data.archive_file.bundle[0].output_path, - pathexpand(var.bundle_config.path) + source = ( + local.bundle_type == "local-folder" + ? data.archive_file.bundle[0].output_path + : pathexpand(var.bundle_config.path) + ) + source_md5hash = ( + local.bundle_type == "local-folder" + ? data.archive_file.bundle[0].output_md5 + : filemd5(pathexpand(var.bundle_config.path)) ) } diff --git a/modules/cloud-function-v2/main.tf b/modules/cloud-function-v2/main.tf index c42f81bf1..db964075f 100644 --- a/modules/cloud-function-v2/main.tf +++ b/modules/cloud-function-v2/main.tf @@ -1,5 +1,5 @@ /** - * Copyright 2024 Google LLC + * 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. @@ -15,6 +15,12 @@ */ locals { + _ctx_p = "$" + ctx = { + for k, v in var.context : k => { + for kk, vv in v : "${local._ctx_p}${k}:${kk}" => vv + } if k != "condition_vars" + } bucket = ( var.bucket_config == null ? var.bucket_name @@ -24,12 +30,9 @@ locals { : null ) ) - prefix = var.prefix == null ? "" : "${var.prefix}-" - service_account_email = ( - var.service_account_create - ? google_service_account.service_account[0].email - : var.service_account - ) + location = lookup(local.ctx.locations, var.region, var.region) + prefix = var.prefix == null ? "" : "${var.prefix}-" + project_id = lookup(local.ctx.project_ids, var.project_id, var.project_id) trigger_sa_create = ( try(var.trigger_config.service_account_create, false) == true ) @@ -50,12 +53,17 @@ locals { } resource "google_vpc_access_connector" "connector" { - count = var.vpc_connector.create == true ? 1 : 0 - project = var.project_id - name = var.vpc_connector.name - region = var.region - ip_cidr_range = var.vpc_connector_config.ip_cidr_range - network = var.vpc_connector_config.network + count = var.vpc_connector.create == true ? 1 : 0 + project = local.project_id + name = var.vpc_connector.name + region = local.location + ip_cidr_range = lookup(local.ctx.cidr_ranges, + var.vpc_connector_config.ip_cidr_range, + var.vpc_connector_config.ip_cidr_range + ) + network = lookup(local.ctx.networks, + var.vpc_connector_config.network, var.vpc_connector_config.network + ) max_instances = try(var.vpc_connector_config.instances.max, null) min_instances = try(var.vpc_connector_config.instances.min, null) max_throughput = try(var.vpc_connector_config.throughput.max, null) @@ -64,11 +72,11 @@ resource "google_vpc_access_connector" "connector" { resource "google_cloudfunctions2_function" "function" { provider = google-beta - project = var.project_id - location = var.region + project = local.project_id + location = local.location name = "${local.prefix}${var.name}" description = var.description - kms_key_name = var.kms_key + kms_key_name = var.kms_key == null ? null : lookup(local.ctx.kms_keys, var.kms_key, var.kms_key) build_config { service_account = var.build_service_account worker_pool = var.build_worker_pool @@ -209,13 +217,6 @@ resource "google_cloud_run_service_iam_member" "invoker" { } } -resource "google_service_account" "service_account" { - count = var.service_account_create ? 1 : 0 - project = var.project_id - account_id = "tf-cf-${var.name}" - display_name = "Terraform Cloud Function ${var.name}." -} - resource "google_service_account" "trigger_service_account" { count = local.trigger_sa_create ? 1 : 0 project = var.project_id diff --git a/modules/cloud-function-v2/serviceaccount.tf b/modules/cloud-function-v2/serviceaccount.tf new file mode 100644 index 000000000..f1fdec866 --- /dev/null +++ b/modules/cloud-function-v2/serviceaccount.tf @@ -0,0 +1,53 @@ +/** + * 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 { + service_account_email = ( + var.service_account_config.create + ? google_service_account.service_account[0].email + : lookup( + local.ctx.iam_principals, + var.service_account_config.email, + var.service_account_config.email + ) + ) + service_account_roles = [ + for role in var.service_account_config.roles + : lookup(local.ctx.custom_roles, role, role) + ] +} + +resource "google_service_account" "service_account" { + count = var.service_account_config.create ? 1 : 0 + project = local.project_id + account_id = coalesce(var.service_account_config.name, var.name) + display_name = coalesce( + var.service_account_config.display_name, + var.service_account_config.name, + var.name + ) +} + +resource "google_project_iam_member" "default" { + for_each = ( + var.service_account_config.create + ? toset(local.service_account_roles) + : toset([]) + ) + role = each.key + project = local.project_id + member = google_service_account.service_account[0].member +} diff --git a/modules/cloud-function-v2/variables-serviceaccount.tf b/modules/cloud-function-v2/variables-serviceaccount.tf new file mode 100644 index 000000000..878feed9f --- /dev/null +++ b/modules/cloud-function-v2/variables-serviceaccount.tf @@ -0,0 +1,31 @@ +/** + * 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. + */ + +variable "service_account_config" { + description = "Service account configurations." + type = object({ + create = optional(bool, true) + display_name = optional(string) + email = optional(string) + name = optional(string) + roles = optional(list(string), [ + "roles/logging.logWriter", + "roles/monitoring.metricWriter" + ]) + }) + nullable = false + default = {} +} diff --git a/modules/cloud-function-v2/variables.tf b/modules/cloud-function-v2/variables.tf index ebaf3c7eb..3b992812c 100644 --- a/modules/cloud-function-v2/variables.tf +++ b/modules/cloud-function-v2/variables.tf @@ -81,6 +81,24 @@ variable "bundle_config" { } } +variable "context" { + description = "Context-specific interpolations." + type = object({ + condition_vars = optional(map(map(string)), {}) # not needed here? + cidr_ranges = optional(map(string), {}) + custom_roles = optional(map(string), {}) + iam_principals = optional(map(string), {}) + kms_keys = optional(map(string), {}) + locations = optional(map(string), {}) + networks = optional(map(string), {}) + project_ids = optional(map(string), {}) + subnets = optional(map(string), {}) + tag_values = optional(map(string), {}) # not needed here? + }) + nullable = false + default = {} +} + variable "description" { description = "Optional description." type = string @@ -183,18 +201,6 @@ variable "secrets" { default = {} } -variable "service_account" { - description = "Service account email. Unused if service account is auto-created." - type = string - default = null -} - -variable "service_account_create" { - description = "Auto-create service account." - type = bool - default = false -} - variable "trigger_config" { description = "Function trigger configuration. Leave null for HTTP trigger." type = object({ diff --git a/tests/modules/cloud_function_v2/examples/bucket-creation.yaml b/tests/modules/cloud_function_v2/examples/bucket-creation.yaml index 99cb104e3..e92953ea5 100644 --- a/tests/modules/cloud_function_v2/examples/bucket-creation.yaml +++ b/tests/modules/cloud_function_v2/examples/bucket-creation.yaml @@ -35,7 +35,6 @@ counts: google_cloudfunctions2_function: 1 google_storage_bucket: 1 google_storage_bucket_object: 1 - modules: 1 - resources: 6 + outputs: {} diff --git a/tests/modules/cloud_function_v2/examples/custom-bundle.yaml b/tests/modules/cloud_function_v2/examples/custom-bundle.yaml new file mode 100644 index 000000000..e938995c6 --- /dev/null +++ b/tests/modules/cloud_function_v2/examples/custom-bundle.yaml @@ -0,0 +1,118 @@ +# 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. + +values: + google_project_iam_member.artifact_writer: + condition: [] + member: serviceAccount:123-compute@developer.gserviceaccount.com + project: project-id + role: roles/artifactregistry.createOnPushWriter + google_project_iam_member.bucket_default_compute_account_grant: + condition: [] + member: serviceAccount:123-compute@developer.gserviceaccount.com + project: project-id + role: roles/storage.objectViewer + module.cf-http.data.archive_file.bundle[0]: + exclude_symlink_directories: null + excludes: + - __pycache__ + output_file_mode: '0644' + output_path: bundle.zip + source: [] + source_content: null + source_content_filename: null + source_dir: assets/sample-function/ + source_file: null + type: zip + module.cf-http.google_cloudfunctions2_function.function: + build_config: + - entry_point: main + on_deploy_update_policy: [] + runtime: python310 + source: + - repo_source: [] + storage_source: + - bucket: bucket + worker_pool: null + description: Terraform managed. + effective_labels: + goog-terraform-provisioned: 'true' + event_trigger: [] + kms_key_name: null + labels: null + location: europe-west8 + name: test-cf-http + project: project-id + service_config: + - all_traffic_on_latest_revision: true + available_cpu: '0.166' + available_memory: 256M + binary_authorization_policy: null + environment_variables: + LOG_EXECUTION_ID: 'true' + ingress_settings: ALLOW_ALL + max_instance_count: 1 + min_instance_count: 0 + secret_environment_variables: [] + secret_volumes: [] + service_account_email: test-cf-http@project-id.iam.gserviceaccount.com + timeout_seconds: 180 + vpc_connector: null + vpc_connector_egress_settings: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + module.cf-http.google_project_iam_member.default["roles/logging.logWriter"]: + condition: [] + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + role: roles/logging.logWriter + module.cf-http.google_project_iam_member.default["roles/monitoring.metricWriter"]: + condition: [] + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + role: roles/monitoring.metricWriter + module.cf-http.google_service_account.service_account[0]: + account_id: test-cf-http + create_ignore_already_exists: null + description: null + disabled: false + display_name: test-cf-http + email: test-cf-http@project-id.iam.gserviceaccount.com + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + timeouts: null + module.cf-http.google_storage_bucket_object.bundle[0]: + bucket: bucket + cache_control: null + content_disposition: null + content_encoding: null + content_language: null + customer_encryption: [] + deletion_policy: null + detect_md5hash: null + event_based_hold: null + force_empty_content_type: null + metadata: null + retention: [] + source: bundle.zip + temporary_hold: null + timeouts: null + +counts: + archive_file: 1 + google_cloudfunctions2_function: 1 + google_project_iam_member: 4 + google_service_account: 1 + google_storage_bucket_object: 1 diff --git a/tests/modules/cloud_function_v2/examples/http-trigger.yaml b/tests/modules/cloud_function_v2/examples/http-trigger.yaml new file mode 100644 index 000000000..7cbb5901c --- /dev/null +++ b/tests/modules/cloud_function_v2/examples/http-trigger.yaml @@ -0,0 +1,120 @@ +# 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. + +values: + google_project_iam_member.artifact_writer: + condition: [] + member: serviceAccount:123-compute@developer.gserviceaccount.com + project: project-id + role: roles/artifactregistry.createOnPushWriter + google_project_iam_member.bucket_default_compute_account_grant: + condition: [] + member: serviceAccount:123-compute@developer.gserviceaccount.com + project: project-id + role: roles/storage.objectViewer + module.cf-http.data.archive_file.bundle[0]: + exclude_symlink_directories: null + excludes: null + output_file_mode: '0644' + output_path: /tmp/bundle-project-id-test-cf-http.zip + source: [] + source_content: null + source_content_filename: null + source_dir: assets/sample-function/ + source_file: null + type: zip + module.cf-http.google_cloudfunctions2_function.function: + build_config: + - entry_point: main + on_deploy_update_policy: [] + runtime: python310 + source: + - repo_source: [] + storage_source: + - bucket: bucket + worker_pool: null + description: Terraform managed. + effective_labels: + goog-terraform-provisioned: 'true' + event_trigger: [] + kms_key_name: null + labels: null + location: europe-west8 + name: test-cf-http + project: project-id + service_config: + - all_traffic_on_latest_revision: true + available_cpu: '0.166' + available_memory: 256M + binary_authorization_policy: null + environment_variables: + LOG_EXECUTION_ID: 'true' + ingress_settings: ALLOW_ALL + max_instance_count: 1 + min_instance_count: 0 + secret_environment_variables: [] + secret_volumes: [] + service_account_email: test-cf-http@project-id.iam.gserviceaccount.com + timeout_seconds: 180 + vpc_connector: null + vpc_connector_egress_settings: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + module.cf-http.google_project_iam_member.default["roles/logging.logWriter"]: + condition: [] + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + role: roles/logging.logWriter + module.cf-http.google_project_iam_member.default["roles/monitoring.metricWriter"]: + condition: [] + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + role: roles/monitoring.metricWriter + module.cf-http.google_service_account.service_account[0]: + account_id: test-cf-http + create_ignore_already_exists: null + description: null + disabled: false + display_name: test-cf-http + email: test-cf-http@project-id.iam.gserviceaccount.com + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + timeouts: null + module.cf-http.google_storage_bucket_object.bundle[0]: + bucket: bucket + cache_control: null + content_disposition: null + content_encoding: null + content_language: null + customer_encryption: [] + deletion_policy: null + detect_md5hash: null + event_based_hold: null + force_empty_content_type: null + metadata: null + retention: [] + source: /tmp/bundle-project-id-test-cf-http.zip + temporary_hold: null + timeouts: null + +counts: + archive_file: 1 + google_cloudfunctions2_function: 1 + google_project_iam_member: 4 + google_service_account: 1 + google_storage_bucket_object: 1 + + +outputs: {} diff --git a/tests/modules/cloud_function_v2/examples/iam.yaml b/tests/modules/cloud_function_v2/examples/iam.yaml index c96dbdb34..427d09e4f 100644 --- a/tests/modules/cloud_function_v2/examples/iam.yaml +++ b/tests/modules/cloud_function_v2/examples/iam.yaml @@ -25,12 +25,9 @@ values: module.cf-http.google_storage_bucket_object.bundle[0]: bucket: bucket customer_encryption: [] - detect_md5hash: different hash source: /tmp/bundle-project-id-test-cf-http.zip counts: google_cloud_run_service_iam_binding: 1 google_cloudfunctions2_function: 1 google_storage_bucket_object: 1 - modules: 1 - resources: 6 diff --git a/tests/modules/cloud_function_v2/examples/multiple_functions.yaml b/tests/modules/cloud_function_v2/examples/multiple_functions.yaml index 65b7a18b9..6de069911 100644 --- a/tests/modules/cloud_function_v2/examples/multiple_functions.yaml +++ b/tests/modules/cloud_function_v2/examples/multiple_functions.yaml @@ -22,4 +22,4 @@ counts: google_cloudfunctions2_function: 2 google_storage_bucket_object: 2 modules: 2 - resources: 7 + resources: 13 diff --git a/tests/modules/cloud_function_v2/examples/private-build-pool.yaml b/tests/modules/cloud_function_v2/examples/private-build-pool.yaml new file mode 100644 index 000000000..6ee70850a --- /dev/null +++ b/tests/modules/cloud_function_v2/examples/private-build-pool.yaml @@ -0,0 +1,130 @@ +# 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. + +values: + google_cloudbuild_worker_pool.pool: + annotations: null + display_name: null + location: europe-west9 + name: custom-pool + network_config: [] + private_service_connect: [] + project: project-id + timeouts: null + worker_config: + - disk_size_gb: 100 + machine_type: e2-standard-4 + no_external_ip: false + google_project_iam_member.artifact_writer: + condition: [] + member: serviceAccount:123-compute@developer.gserviceaccount.com + project: project-id + role: roles/artifactregistry.createOnPushWriter + google_project_iam_member.bucket_default_compute_account_grant: + condition: [] + member: serviceAccount:123-compute@developer.gserviceaccount.com + project: project-id + role: roles/storage.objectViewer + module.cf-http.data.archive_file.bundle[0]: + exclude_symlink_directories: null + excludes: null + output_file_mode: '0644' + output_path: /tmp/bundle-project-id-test-cf-http.zip + source: [] + source_content: null + source_content_filename: null + source_dir: assets/sample-function/ + source_file: null + type: zip + module.cf-http.google_cloudfunctions2_function.function: + build_config: + - entry_point: main + on_deploy_update_policy: [] + runtime: python310 + source: + - repo_source: [] + storage_source: + - bucket: bucket + description: Terraform managed. + effective_labels: + goog-terraform-provisioned: 'true' + event_trigger: [] + kms_key_name: null + labels: null + location: europe-west9 + name: test-cf-http + project: project-id + service_config: + - all_traffic_on_latest_revision: true + available_cpu: '0.166' + available_memory: 256M + binary_authorization_policy: null + environment_variables: + LOG_EXECUTION_ID: 'true' + ingress_settings: ALLOW_ALL + max_instance_count: 1 + min_instance_count: 0 + secret_environment_variables: [] + secret_volumes: [] + service_account_email: test-cf-http@project-id.iam.gserviceaccount.com + timeout_seconds: 180 + vpc_connector: null + vpc_connector_egress_settings: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + module.cf-http.google_project_iam_member.default["roles/logging.logWriter"]: + condition: [] + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + role: roles/logging.logWriter + module.cf-http.google_project_iam_member.default["roles/monitoring.metricWriter"]: + condition: [] + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + role: roles/monitoring.metricWriter + module.cf-http.google_service_account.service_account[0]: + account_id: test-cf-http + create_ignore_already_exists: null + description: null + disabled: false + display_name: test-cf-http + email: test-cf-http@project-id.iam.gserviceaccount.com + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + timeouts: null + module.cf-http.google_storage_bucket_object.bundle[0]: + bucket: bucket + cache_control: null + content_disposition: null + content_encoding: null + content_language: null + customer_encryption: [] + deletion_policy: null + detect_md5hash: null + event_based_hold: null + force_empty_content_type: null + metadata: null + retention: [] + source: /tmp/bundle-project-id-test-cf-http.zip + temporary_hold: null + timeouts: null + +counts: + archive_file: 1 + google_cloudbuild_worker_pool: 1 + google_cloudfunctions2_function: 1 + google_project_iam_member: 4 + google_service_account: 1 + google_storage_bucket_object: 1 diff --git a/tests/modules/cloud_function_v2/examples/pubsub-non-http-trigger.yaml b/tests/modules/cloud_function_v2/examples/pubsub-non-http-trigger.yaml new file mode 100644 index 000000000..430a89a3c --- /dev/null +++ b/tests/modules/cloud_function_v2/examples/pubsub-non-http-trigger.yaml @@ -0,0 +1,91 @@ +# 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. + +values: + module.cf-http.google_cloudfunctions2_function.function: + build_config: + - entry_point: main + on_deploy_update_policy: [] + runtime: python310 + source: + - repo_source: [] + storage_source: + - bucket: bucket + worker_pool: null + description: Terraform managed. + effective_labels: + goog-terraform-provisioned: 'true' + event_trigger: + - event_filters: [] + event_type: google.cloud.pubsub.topic.v1.messagePublished + retry_policy: RETRY_POLICY_DO_NOT_RETRY + service_account_email: sa-cloudfunction@project-id.iam.gserviceaccount.com + trigger_region: europe-west8 + kms_key_name: null + labels: null + location: europe-west8 + name: test-cf-http + project: project-id + service_config: + - all_traffic_on_latest_revision: true + available_cpu: '0.166' + available_memory: 256M + binary_authorization_policy: null + environment_variables: + LOG_EXECUTION_ID: 'true' + ingress_settings: ALLOW_ALL + max_instance_count: 1 + min_instance_count: 0 + secret_environment_variables: [] + secret_volumes: [] + service_account_email: test-cf-http@project-id.iam.gserviceaccount.com + timeout_seconds: 180 + vpc_connector: null + vpc_connector_egress_settings: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + module.pubsub.google_pubsub_topic.default: + effective_labels: + goog-terraform-provisioned: 'true' + ingestion_data_source_settings: [] + kms_key_name: null + labels: null + message_retention_duration: null + message_transforms: [] + name: topic + project: project-id + schema_settings: [] + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + module.pubsub.google_pubsub_topic_iam_binding.authoritative["roles/pubsub.subscriber"]: + condition: [] + members: + - serviceAccount:123-compute@developer.gserviceaccount.com + project: project-id + role: roles/pubsub.subscriber + topic: topic + +counts: + archive_file: 1 + google_cloudfunctions2_function: 1 + google_project_iam_member: 5 + google_pubsub_topic: 1 + google_pubsub_topic_iam_binding: 1 + google_service_account: 2 + google_storage_bucket_object: 1 + + +outputs: {} diff --git a/tests/modules/cloud_function_v2/examples/secrets.yaml b/tests/modules/cloud_function_v2/examples/secrets.yaml index 77a36c8c6..04bf0be8f 100644 --- a/tests/modules/cloud_function_v2/examples/secrets.yaml +++ b/tests/modules/cloud_function_v2/examples/secrets.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# 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. @@ -27,9 +27,55 @@ values: # secret: var_secret # known after apply versions: - {} + service_account_email: test-cf-http@project-id.iam.gserviceaccount.com + module.cf-http.google_service_account.service_account[0]: + account_id: test-cf-http + create_ignore_already_exists: null + description: null + disabled: false + display_name: test-cf-http + email: test-cf-http@project-id.iam.gserviceaccount.com + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + timeouts: null + module.secret-manager.google_secret_manager_secret.default["credentials"]: + annotations: null + deletion_protection: false + effective_labels: + goog-terraform-provisioned: 'true' + labels: null + project: project-id + replication: + - auto: + - customer_managed_encryption: [] + user_managed: [] + rotation: [] + secret_id: credentials + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + topics: [] + ttl: null + version_aliases: null + version_destroy_ttl: null + module.secret-manager.google_secret_manager_secret_iam_binding.authoritative["credentials.roles/secretmanager.secretAccessor"]: + condition: [] + members: + - serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + role: roles/secretmanager.secretAccessor + module.secret-manager.google_secret_manager_secret_version.default["credentials/v1"]: + deletion_policy: DELETE + enabled: true + is_secret_data_base64: false + secret_data: manual foo bar spam + secret_data_wo: null + secret_data_wo_version: 0 + timeouts: null counts: google_cloudfunctions2_function: 1 - google_storage_bucket_object: 1 - modules: 2 - resources: 8 + google_secret_manager_secret: 1 + google_secret_manager_secret_iam_binding: 1 + google_secret_manager_secret_version: 1 + google_service_account: 1 diff --git a/tests/modules/cloud_function_v2/examples/service-account-1.yaml b/tests/modules/cloud_function_v2/examples/service-account-1.yaml new file mode 100644 index 000000000..ba3c7d8b6 --- /dev/null +++ b/tests/modules/cloud_function_v2/examples/service-account-1.yaml @@ -0,0 +1,80 @@ +# 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. + +values: + module.cf-http.google_cloudfunctions2_function.function: + build_config: + - entry_point: main + on_deploy_update_policy: [] + runtime: python310 + source: + - repo_source: [] + storage_source: + - bucket: bucket + worker_pool: null + description: Terraform managed. + effective_labels: + goog-terraform-provisioned: 'true' + event_trigger: [] + kms_key_name: null + labels: null + location: europe-west8 + name: test-cf-http + project: project-id + service_config: + - all_traffic_on_latest_revision: true + available_cpu: '0.166' + available_memory: 256M + binary_authorization_policy: null + environment_variables: + LOG_EXECUTION_ID: 'true' + ingress_settings: ALLOW_ALL + max_instance_count: 1 + min_instance_count: 0 + secret_environment_variables: [] + secret_volumes: [] + service_account_email: test-cf-http@project-id.iam.gserviceaccount.com + timeout_seconds: 180 + vpc_connector: null + vpc_connector_egress_settings: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + module.cf-http.google_project_iam_member.default["roles/logging.logWriter"]: + condition: [] + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + role: roles/logging.logWriter + module.cf-http.google_project_iam_member.default["roles/monitoring.metricWriter"]: + condition: [] + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + role: roles/monitoring.metricWriter + module.cf-http.google_service_account.service_account[0]: + account_id: test-cf-http + create_ignore_already_exists: null + description: null + disabled: false + display_name: test-cf-http + email: test-cf-http@project-id.iam.gserviceaccount.com + member: serviceAccount:test-cf-http@project-id.iam.gserviceaccount.com + project: project-id + timeouts: null + +counts: + archive_file: 1 + google_cloudfunctions2_function: 1 + google_project_iam_member: 4 + google_service_account: 1 + google_storage_bucket_object: 1 diff --git a/tests/modules/cloud_function_v2/examples/service-account-2.yaml b/tests/modules/cloud_function_v2/examples/service-account-2.yaml new file mode 100644 index 000000000..2a95f05c0 --- /dev/null +++ b/tests/modules/cloud_function_v2/examples/service-account-2.yaml @@ -0,0 +1,58 @@ +# 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. + +values: + module.cf-http.google_cloudfunctions2_function.function: + build_config: + - entry_point: main + on_deploy_update_policy: [] + runtime: python310 + source: + - repo_source: [] + storage_source: + - bucket: bucket + worker_pool: null + description: Terraform managed. + effective_labels: + goog-terraform-provisioned: 'true' + event_trigger: [] + kms_key_name: null + labels: null + location: europe-west8 + name: test-cf-http + project: project-id + service_config: + - all_traffic_on_latest_revision: true + available_cpu: '0.166' + available_memory: 256M + binary_authorization_policy: null + environment_variables: + LOG_EXECUTION_ID: 'true' + ingress_settings: ALLOW_ALL + max_instance_count: 1 + min_instance_count: 0 + secret_environment_variables: [] + secret_volumes: [] + service_account_email: sa1@sa.example + timeout_seconds: 180 + vpc_connector: null + vpc_connector_egress_settings: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + +counts: + google_cloudfunctions2_function: 1 + google_project_iam_member: 2 + google_service_account: 0 diff --git a/tools/duplicate-diff.py b/tools/duplicate-diff.py index ef1219774..c22c93b48 100755 --- a/tools/duplicate-diff.py +++ b/tools/duplicate-diff.py @@ -28,12 +28,33 @@ duplicates = [ # "fast/stages/2-networking-a-simple/data/cidrs.yaml", "fast/stages/2-networking-b-nva/data/cidrs.yaml", "fast/stages/2-networking-c-separate-envs/data/cidrs.yaml", + ], + [ + "modules/cloud-function-v1/serviceaccount.tf", + "modules/cloud-function-v2/serviceaccount.tf", + ], + [ + "modules/cloud-function-v1/variables-serviceaccount.tf", + "modules/cloud-function-v2/variables-serviceaccount.tf", + ], + [ + "modules/cloud-function-v1/bundle.tf", + "modules/cloud-function-v2/bundle.tf", ] ] -for group in duplicates: - first = group[0] - for second in group[1:]: - if not filecmp.cmp(first, second): # true if files are the same - print(f'found diff between {first} and {second}') - sys.exit(1) + +def main(): + error = False + for group in duplicates: + first = group[0] + for second in group[1:]: + if not filecmp.cmp(first, second): # true if files are the same + print(f'found diff between {first} and {second}') + error = True + if error: + sys.exit(1) + + +if __name__ == '__main__': + main() From d1f257bcb9657bf4344a31b80421334976f6cd45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Mon, 20 Oct 2025 11:32:03 +0000 Subject: [PATCH 5/9] align project-templates to new functions interface --- .../secops-anonymization-pipeline/README.md | 6 ++--- .../secops-anonymization-pipeline/main.tf | 22 ++++++++++--------- .../api-gateway/recipe-multi-region/README.md | 2 +- .../api-gateway/recipe-multi-region/main.tf | 4 +++- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/fast/project-templates/secops-anonymization-pipeline/README.md b/fast/project-templates/secops-anonymization-pipeline/README.md index 8be973103..1c78daa7a 100644 --- a/fast/project-templates/secops-anonymization-pipeline/README.md +++ b/fast/project-templates/secops-anonymization-pipeline/README.md @@ -22,7 +22,7 @@ The following diagram illustrates the high-level design of the solution, which c The use case is a SecOps deployment composed of 2 tenants (one for production and one for development/testing). There might be the need to export production data from the prod tenant and import them back in DEV (possibly anonymizing it) for rules and/or parser development, that is why this pipeline might be convenient for speeding up the data migration process. -The solution is based on a custom Python script responsible for implementing the aforementioned logic. The script leverages the new [SecOps API Wrapper](https://github.com/google/secops-wrapper) available also in [PyPi](https://pypi.org/project/secops/). +The solution is based on a custom Python script responsible for implementing the aforementioned logic. The script leverages the new [SecOps API Wrapper](https://github.com/google/secops-wrapper) available also in [PyPi](https://pypi.org/project/secops/). ### Pipeline Steps @@ -93,7 +93,7 @@ terraform apply #### Step 5: Test solution -Test the solution triggering an export from the Cloud Scheduler page, after few hours (accoding to the size of the export) logs should be available on secops-export bucket. Please check for any issue during export using the corresponding APIs and the export ID. +Test the solution triggering an export from the Cloud Scheduler page, after few hours (according to the size of the export) logs should be available on secops-export bucket. Please check for any issue during export using the corresponding APIs and the export ID. ## Variables @@ -145,5 +145,5 @@ module "test" { secondary = "europe-west1" } } -# tftest modules=7 resources=49 +# tftest modules=7 resources=51 ``` diff --git a/fast/project-templates/secops-anonymization-pipeline/main.tf b/fast/project-templates/secops-anonymization-pipeline/main.tf index fc39cc27f..046d49a19 100644 --- a/fast/project-templates/secops-anonymization-pipeline/main.tf +++ b/fast/project-templates/secops-anonymization-pipeline/main.tf @@ -123,16 +123,18 @@ module "anonymized-bucket" { } module "function" { - source = "../../../modules/cloud-function-v2" - project_id = module.project.project_id - region = var.regions.primary - prefix = var.prefix - name = "secops-anonymization" - bucket_name = "${var.project_id}-anonymization" - service_account_create = true - ingress_settings = "ALLOW_INTERNAL_AND_GCLB" - build_worker_pool = var.cloud_function_config.build_worker_pool_id - build_service_account = var.cloud_function_config.build_sa != null ? var.cloud_function_config.build_sa : module.cloudbuild-sa.0.id + source = "../../../modules/cloud-function-v2" + project_id = module.project.project_id + region = var.regions.primary + prefix = var.prefix + name = "secops-anonymization" + bucket_name = "${var.project_id}-anonymization" + service_account_config = { + create = true + } + ingress_settings = "ALLOW_INTERNAL_AND_GCLB" + build_worker_pool = var.cloud_function_config.build_worker_pool_id + build_service_account = var.cloud_function_config.build_sa != null ? var.cloud_function_config.build_sa : module.cloudbuild-sa.0.id bucket_config = { lifecycle_delete_age_days = 1 } diff --git a/modules/api-gateway/recipe-multi-region/README.md b/modules/api-gateway/recipe-multi-region/README.md index 152bb1bf8..e2811f081 100644 --- a/modules/api-gateway/recipe-multi-region/README.md +++ b/modules/api-gateway/recipe-multi-region/README.md @@ -43,5 +43,5 @@ module "test" { number = 1234567890 } } -# tftest modules=8 resources=43 +# tftest modules=8 resources=47 ``` diff --git a/modules/api-gateway/recipe-multi-region/main.tf b/modules/api-gateway/recipe-multi-region/main.tf index e48dd364e..25a813b32 100644 --- a/modules/api-gateway/recipe-multi-region/main.tf +++ b/modules/api-gateway/recipe-multi-region/main.tf @@ -95,7 +95,9 @@ module "functions" { entry_point = "helloGET" runtime = "nodejs22" } - service_account_create = true + service_account_config = { + create = true + } iam = { "roles/run.invoker" = [module.sa.iam_email] } From b67b121a28b1c7e0cd2f53f5834777199ced2bc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Mon, 20 Oct 2025 15:22:26 +0000 Subject: [PATCH 6/9] Context tests for Cloud Function v1 --- .../modules/cloud_function_v1/context.tfvars | 56 +++++++++++++++++ tests/modules/cloud_function_v1/context.yaml | 60 +++++++++++++++++++ tests/modules/cloud_function_v1/tftest.yaml | 1 + 3 files changed, 117 insertions(+) create mode 100644 tests/modules/cloud_function_v1/context.tfvars create mode 100644 tests/modules/cloud_function_v1/context.yaml diff --git a/tests/modules/cloud_function_v1/context.tfvars b/tests/modules/cloud_function_v1/context.tfvars new file mode 100644 index 000000000..558271b35 --- /dev/null +++ b/tests/modules/cloud_function_v1/context.tfvars @@ -0,0 +1,56 @@ +name = "test-cf-kms" +bucket_name = "bucket" +bundle_config = { + path = "gs://assets/sample-function.zip" +} +context = { + cidr_ranges = { + test = "10.10.20.0/28" + } + custom_roles = { + myrole_one = "organizations/366118655033/roles/myRoleOne" + } + iam_principals = { + mygroup = "group:test-group@example.com" + } + kms_keys = { + test = "projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute" + } + locations = { + ew8 = "europe-west8" + } + networks = { + test = "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0" + } + project_ids = { + test = "foo-test-0" + } + subnets = { + test = "projects/foo-dev-net-spoke-0/regions/europe-west1/subnetworks/gce" + } +} +kms_key = "$kms_keys:test" +iam = { + "$custom_roles:myrole_one" = [ + "$iam_principals:mygroup" + ] +} +project_id = "$project_ids:test" +region = "$locations:ew8" +service_account_config = { + roles = [ + "$custom_roles:myrole_one" + ] +} +vpc_connector = { + create = true + name = "connector_name" +} +vpc_connector_config = { + ip_cidr_range = "$cidr_ranges:test" + network = "$networks:test" + instances = { + max = 10 + min = 3 + } +} diff --git a/tests/modules/cloud_function_v1/context.yaml b/tests/modules/cloud_function_v1/context.yaml new file mode 100644 index 000000000..6417d4548 --- /dev/null +++ b/tests/modules/cloud_function_v1/context.yaml @@ -0,0 +1,60 @@ +# 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. + +values: + google_cloudfunctions_function.function: + kms_key_name: projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute + project: foo-test-0 + region: europe-west8 + service_account_email: test-cf-kms@foo-test-0.iam.gserviceaccount.com + google_cloudfunctions_function_iam_binding.default["$custom_roles:myrole_one"]: + condition: [] + members: + - group:test-group@example.com + project: foo-test-0 + region: europe-west8 + role: organizations/366118655033/roles/myRoleOne + google_project_iam_member.default["organizations/366118655033/roles/myRoleOne"]: + condition: [] + member: serviceAccount:test-cf-kms@foo-test-0.iam.gserviceaccount.com + project: foo-test-0 + role: organizations/366118655033/roles/myRoleOne + google_service_account.service_account[0]: + account_id: test-cf-kms + create_ignore_already_exists: null + description: null + disabled: false + display_name: test-cf-kms + email: test-cf-kms@foo-test-0.iam.gserviceaccount.com + member: serviceAccount:test-cf-kms@foo-test-0.iam.gserviceaccount.com + project: foo-test-0 + timeouts: null + google_vpc_access_connector.connector[0]: + ip_cidr_range: 10.10.20.0/28 + machine_type: e2-micro + max_instances: 10 + min_instances: 3 + name: connector_name + network: projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0 + project: foo-test-0 + region: europe-west8 + subnet: [] + timeouts: null + +counts: + google_cloudfunctions_function: 1 + google_cloudfunctions_function_iam_binding: 1 + google_project_iam_member: 1 + google_service_account: 1 + google_vpc_access_connector: 1 diff --git a/tests/modules/cloud_function_v1/tftest.yaml b/tests/modules/cloud_function_v1/tftest.yaml index 448e96615..ea15fe700 100644 --- a/tests/modules/cloud_function_v1/tftest.yaml +++ b/tests/modules/cloud_function_v1/tftest.yaml @@ -14,4 +14,5 @@ module: modules/cloud-function-v1 tests: + context: kms: From d9029e47a0c8bd3db1affcfc79fbfe65e3c9c3dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Mon, 20 Oct 2025 21:46:11 +0000 Subject: [PATCH 7/9] VPC Connector alignment to Cloud Run v2 + contexts --- modules/cloud-function-v1/README.md | 86 ++++++++++++++++++- modules/cloud-function-v1/main.tf | 62 ++++--------- .../variables-vpcconnector.tf | 59 +++++++++++++ modules/cloud-function-v1/variables.tf | 36 -------- modules/cloud-function-v1/vpcconnector.tf | 74 ++++++++++++++++ modules/cloud-function-v2/README.md | 86 ++++++++++++++++++- modules/cloud-function-v2/main.tf | 62 ++++--------- .../variables-vpcconnector.tf | 59 +++++++++++++ modules/cloud-function-v2/variables.tf | 36 -------- modules/cloud-function-v2/vpcconnector.tf | 74 ++++++++++++++++ modules/cloud-run-v2/README.md | 2 +- tests/fixtures/vpc-connector.tf | 2 +- .../context-subnet-project.tfvars | 58 +++++++++++++ .../context-subnet-project.yaml | 61 +++++++++++++ .../cloud_function_v1/context-subnet.tfvars | 57 ++++++++++++ .../cloud_function_v1/context-subnet.yaml | 60 +++++++++++++ .../modules/cloud_function_v1/context.tfvars | 2 +- tests/modules/cloud_function_v1/context.yaml | 2 +- ...vpc-access-connector-create-sharedvpc.yaml | 68 +++++++++++++++ .../service-vpc-access-connector-create.yaml | 56 ++++++++++++ .../service-vpc-access-connector.yaml | 56 ++++++++++++ tests/modules/cloud_function_v1/tftest.yaml | 2 + .../context-subnet-project.tfvars | 58 +++++++++++++ .../context-subnet-project.yaml | 64 ++++++++++++++ .../cloud_function_v2/context-subnet.tfvars | 57 ++++++++++++ .../cloud_function_v2/context-subnet.yaml | 62 +++++++++++++ .../modules/cloud_function_v2/context.tfvars | 56 ++++++++++++ tests/modules/cloud_function_v2/context.yaml | 61 +++++++++++++ ...vpc-access-connector-create-sharedvpc.yaml | 73 ++++++++++++++++ .../service-vpc-access-connector-create.yaml | 62 +++++++++++++ .../service-vpc-access-connector.yaml | 62 +++++++++++++ tests/modules/cloud_function_v2/kms.tfvars | 9 ++ tests/modules/cloud_function_v2/kms.yaml | 44 ++++++++++ tests/modules/cloud_function_v2/tftest.yaml | 21 +++++ .../cloud_function_v2/vpcconnector.tfvars | 11 +++ .../cloud_function_v2/vpcconnector.yaml | 44 ++++++++++ .../service-vpc-access-connector.yaml | 3 +- tools/duplicate-diff.py | 14 ++- 38 files changed, 1588 insertions(+), 173 deletions(-) create mode 100644 modules/cloud-function-v1/variables-vpcconnector.tf create mode 100644 modules/cloud-function-v1/vpcconnector.tf create mode 100644 modules/cloud-function-v2/variables-vpcconnector.tf create mode 100644 modules/cloud-function-v2/vpcconnector.tf create mode 100644 tests/modules/cloud_function_v1/context-subnet-project.tfvars create mode 100644 tests/modules/cloud_function_v1/context-subnet-project.yaml create mode 100644 tests/modules/cloud_function_v1/context-subnet.tfvars create mode 100644 tests/modules/cloud_function_v1/context-subnet.yaml create mode 100644 tests/modules/cloud_function_v1/examples/service-vpc-access-connector-create-sharedvpc.yaml create mode 100644 tests/modules/cloud_function_v1/examples/service-vpc-access-connector-create.yaml create mode 100644 tests/modules/cloud_function_v1/examples/service-vpc-access-connector.yaml create mode 100644 tests/modules/cloud_function_v2/context-subnet-project.tfvars create mode 100644 tests/modules/cloud_function_v2/context-subnet-project.yaml create mode 100644 tests/modules/cloud_function_v2/context-subnet.tfvars create mode 100644 tests/modules/cloud_function_v2/context-subnet.yaml create mode 100644 tests/modules/cloud_function_v2/context.tfvars create mode 100644 tests/modules/cloud_function_v2/context.yaml create mode 100644 tests/modules/cloud_function_v2/examples/service-vpc-access-connector-create-sharedvpc.yaml create mode 100644 tests/modules/cloud_function_v2/examples/service-vpc-access-connector-create.yaml create mode 100644 tests/modules/cloud_function_v2/examples/service-vpc-access-connector.yaml create mode 100644 tests/modules/cloud_function_v2/kms.tfvars create mode 100644 tests/modules/cloud_function_v2/kms.yaml create mode 100644 tests/modules/cloud_function_v2/tftest.yaml create mode 100644 tests/modules/cloud_function_v2/vpcconnector.tfvars create mode 100644 tests/modules/cloud_function_v2/vpcconnector.yaml diff --git a/modules/cloud-function-v1/README.md b/modules/cloud-function-v1/README.md index 47baab320..158522750 100644 --- a/modules/cloud-function-v1/README.md +++ b/modules/cloud-function-v1/README.md @@ -15,6 +15,7 @@ Cloud Function management, with support for IAM roles, optional bucket creation - [Multiple Cloud Functions within project](#multiple-cloud-functions-within-project) - [Mounting secrets from Secret Manager](#mounting-secrets-from-secret-manager) - [Using CMEK to encrypt function resources](#using-cmek-to-encrypt-function-resources) +- [VPC Access Connector](#vpc-access-connector) - [Variables](#variables) - [Outputs](#outputs) - [Fixtures](#fixtures) @@ -393,6 +394,85 @@ module "cf-http" { } # tftest inventory=cmek.yaml ``` + +## VPC Access Connector + +You can use an existing [VPC Access Connector](https://cloud.google.com/vpc/docs/serverless-vpc-access) to connect to a VPC from Cloud Run. + +```hcl +module "cf_http" { + source = "./fabric/modules/cloud-function-v1" + project_id = var.project_id + region = var.region + name = "test-cf-http" + bucket_name = var.bucket + bundle_config = { + path = "assets/sample-function/" + } + vpc_connector = { + name = google_vpc_access_connector.connector.id + egress_setting = "ALL_TRAFFIC" + } +} +# tftest fixtures=fixtures/vpc-connector.tf inventory=service-vpc-access-connector.yaml e2e +``` + +If creation of the VPC Access Connector is required, use the `vpc_connector.create` and `vpc_connector_create` variable which also supports optional attributes like number of instances, machine type, or throughput. + +```hcl +module "cf_http" { + source = "./fabric/modules/cloud-function-v1" + project_id = var.project_id + region = var.region + name = "test-cf-http" + bucket_name = var.bucket + bundle_config = { + path = "assets/sample-function/" + } + vpc_connector = { + create = true + } + vpc_connector_create = { + ip_cidr_range = "10.10.10.0/28" + network = var.vpc.self_link + instances = { + max = 10 + min = 3 + } + } +} +# tftest inventory=service-vpc-access-connector-create.yaml e2e +``` + +Note that if you are using a Shared VPC for the connector, you need to specify a subnet and the host project if this is not where the Cloud Run service is deployed. + +```hcl +module "cf_http" { + source = "./fabric/modules/cloud-function-v1" + project_id = var.project_id + region = var.region + name = "test-cf-http" + bucket_name = var.bucket + bundle_config = { + path = "assets/sample-function/" + } + vpc_connector = { + create = true + } + vpc_connector_create = { + machine_type = "e2-standard-4" + subnet = { + name = module.net-vpc-host.subnets["${var.region}/fixture-subnet-28"].name + project_id = module.project-host.project_id + } + throughput = { + max = 300 + min = 200 + } + } +} +# tftest fixtures=fixtures/shared-vpc.tf inventory=service-vpc-access-connector-create-sharedvpc.yaml e2e +``` ## Variables @@ -420,8 +500,8 @@ module "cf-http" { | [secrets](variables.tf#L194) | Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format. | map(object({…})) | | {} | | [service_account_config](variables-serviceaccount.tf#L17) | Service account configurations. | object({…}) | | {} | | [trigger_config](variables.tf#L206) | Function trigger configuration. Leave null for HTTP trigger. | object({…}) | | null | -| [vpc_connector](variables.tf#L216) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | {} | -| [vpc_connector_config](variables.tf#L227) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | +| [vpc_connector](variables-vpcconnector.tf#L17) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | {} | +| [vpc_connector_create](variables-vpcconnector.tf#L28) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | ## Outputs @@ -443,4 +523,6 @@ module "cf-http" { - [cloudbuild-custom-pool.tf](../../tests/fixtures/cloudbuild-custom-pool.tf) - [functions-default-sa-iam-grants.tf](../../tests/fixtures/functions-default-sa-iam-grants.tf) - [pubsub.tf](../../tests/fixtures/pubsub.tf) +- [shared-vpc.tf](../../tests/fixtures/shared-vpc.tf) +- [vpc-connector.tf](../../tests/fixtures/vpc-connector.tf) diff --git a/modules/cloud-function-v1/main.tf b/modules/cloud-function-v1/main.tf index 5d215226c..4c6cc0f07 100644 --- a/modules/cloud-function-v1/main.tf +++ b/modules/cloud-function-v1/main.tf @@ -30,36 +30,10 @@ locals { : null ) ) - location = lookup(local.ctx.locations, var.region, var.region) - prefix = var.prefix == null ? "" : "${var.prefix}-" - project_id = lookup(local.ctx.project_ids, var.project_id, var.project_id) - vpc_connector = ( - var.vpc_connector.name == null - ? null - : ( - var.vpc_connector.create == false - ? var.vpc_connector.name - : google_vpc_access_connector.connector[0].id - ) - ) -} - -resource "google_vpc_access_connector" "connector" { - count = var.vpc_connector.create == true ? 1 : 0 - project = local.project_id - name = var.vpc_connector.name - region = local.location - ip_cidr_range = lookup(local.ctx.cidr_ranges, - var.vpc_connector_config.ip_cidr_range, - var.vpc_connector_config.ip_cidr_range - ) - network = lookup(local.ctx.networks, - var.vpc_connector_config.network, var.vpc_connector_config.network - ) - max_instances = try(var.vpc_connector_config.instances.max, null) - min_instances = try(var.vpc_connector_config.instances.min, null) - max_throughput = try(var.vpc_connector_config.throughput.max, null) - min_throughput = try(var.vpc_connector_config.throughput.min, null) + location = lookup(local.ctx.locations, var.region, var.region) + prefix = var.prefix == null ? "" : "${var.prefix}-" + project_id = lookup(local.ctx.project_ids, var.project_id, var.project_id) + vpc_connector = var.vpc_connector.create ? google_vpc_access_connector.connector[0].id : var.vpc_connector.name } resource "google_cloudfunctions_function" "function" { @@ -80,19 +54,17 @@ resource "google_cloudfunctions_function" "function" { ? replace(var.bundle_config.path, "/^gs:\\/\\/[^\\/]+\\//", "") : google_storage_bucket_object.bundle[0].name ) - labels = var.labels - trigger_http = var.trigger_config == null ? true : null - https_trigger_security_level = var.https_security_level == null ? "SECURE_ALWAYS" : var.https_security_level - ingress_settings = var.ingress_settings - build_worker_pool = var.build_worker_pool - build_environment_variables = var.build_environment_variables - kms_key_name = var.kms_key == null ? null : lookup(local.ctx.kms_keys, var.kms_key, var.kms_key) - docker_registry = try(var.repository_settings.registry, "ARTIFACT_REGISTRY") - docker_repository = try(var.repository_settings.repository, null) - vpc_connector = local.vpc_connector - vpc_connector_egress_settings = try( - var.vpc_connector.egress_settings, null - ) + labels = var.labels + trigger_http = var.trigger_config == null ? true : null + https_trigger_security_level = var.https_security_level == null ? "SECURE_ALWAYS" : var.https_security_level + ingress_settings = var.ingress_settings + build_worker_pool = var.build_worker_pool + build_environment_variables = var.build_environment_variables + kms_key_name = var.kms_key == null ? null : lookup(local.ctx.kms_keys, var.kms_key, var.kms_key) + docker_registry = try(var.repository_settings.registry, "ARTIFACT_REGISTRY") + docker_repository = try(var.repository_settings.repository, null) + vpc_connector = local.vpc_connector + vpc_connector_egress_settings = var.vpc_connector.egress_settings dynamic "event_trigger" { for_each = var.trigger_config == null ? [] : [""] @@ -143,8 +115,8 @@ resource "google_cloudfunctions_function_iam_binding" "default" { project = local.project_id region = local.location cloud_function = google_cloudfunctions_function.function.id - role = each.key - members = each.value + role = lookup(local.ctx.custom_roles, each.key, each.key) + members = [for member in each.value : lookup(local.ctx.iam_principals, member, member)] lifecycle { replace_triggered_by = [google_cloudfunctions_function.function] } diff --git a/modules/cloud-function-v1/variables-vpcconnector.tf b/modules/cloud-function-v1/variables-vpcconnector.tf new file mode 100644 index 000000000..967ce6983 --- /dev/null +++ b/modules/cloud-function-v1/variables-vpcconnector.tf @@ -0,0 +1,59 @@ +/** + * 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 "vpc_connector" { + description = "VPC connector configuration. Set create to 'true' if a new connector needs to be created." + type = object({ + create = optional(bool, false) + name = optional(string) + egress_settings = optional(string) + }) + nullable = false + default = {} +} + +variable "vpc_connector_create" { + description = "VPC connector network configuration. Must be provided if new VPC connector is being created." + type = object({ + ip_cidr_range = optional(string) + machine_type = optional(string) + name = optional(string) + network = optional(string) + instances = optional(object({ + max = optional(number) + min = optional(number) + }), {} + ) + throughput = optional(object({ + max = optional(number) + min = optional(number) + }), {} + ) + subnet = optional(object({ + name = optional(string) + project_id = optional(string) + }), {}) + }) + default = null + validation { + condition = ( + var.vpc_connector.create == false || + try(var.vpc_connector_create.instances, null) != null || + try(var.vpc_connector_create.throughput, null) != null + ) + error_message = "VPC connector must specify either instances or throughput." + } +} diff --git a/modules/cloud-function-v1/variables.tf b/modules/cloud-function-v1/variables.tf index 12843bc59..c089bbb07 100644 --- a/modules/cloud-function-v1/variables.tf +++ b/modules/cloud-function-v1/variables.tf @@ -212,39 +212,3 @@ variable "trigger_config" { }) default = null } - -variable "vpc_connector" { - description = "VPC connector configuration. Set create to 'true' if a new connector needs to be created." - type = object({ - create = optional(bool, false) - name = optional(string) - egress_settings = optional(string) - }) - nullable = false - default = {} -} - -variable "vpc_connector_config" { - description = "VPC connector network configuration. Must be provided if new VPC connector is being created." - type = object({ - ip_cidr_range = string - network = string - instances = optional(object({ - max = optional(number) - min = optional(number, 2) - })) - throughput = optional(object({ - max = optional(number, 300) - min = optional(number, 200) - })) - }) - default = null - validation { - condition = ( - var.vpc_connector_config == null || - try(var.vpc_connector_config.instances, null) != null || - try(var.vpc_connector_config.throughput, null) != null - ) - error_message = "VPC connector must specify either instances or throughput." - } -} diff --git a/modules/cloud-function-v1/vpcconnector.tf b/modules/cloud-function-v1/vpcconnector.tf new file mode 100644 index 000000000..4d01eada0 --- /dev/null +++ b/modules/cloud-function-v1/vpcconnector.tf @@ -0,0 +1,74 @@ +/** + * 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 { + _connector_subnet_name_ctx = ( + try(var.vpc_connector_create.subnet.name, null) == null ? false : + contains(keys(local.ctx.subnets), var.vpc_connector_create.subnet.name) + ) + # if you pass the subnet, you must pass only the name, not the whole id + _connector_subnet_name = ( + local._connector_subnet_name_ctx + ? provider::google::name_from_id(local.ctx.subnets[var.vpc_connector_create.subnet.name]) + : try(var.vpc_connector_create.subnet.name, null) + ) + # if project is not provided, but subnet is coming from context, use project from subnet id in context + # and avoid lookups using null project + _connector_subnet_project_input = try(var.vpc_connector_create.subnet.project_id, null) + _connector_subnet_project = ( + local._connector_subnet_project_input == null + ? ( + local._connector_subnet_name_ctx + ? provider::google::project_from_id(local.ctx.subnets[var.vpc_connector_create.subnet.name]) + : null + ) + : lookup( + local.ctx.project_ids, local._connector_subnet_project_input, + local._connector_subnet_project_input + ) + ) +} + +resource "google_vpc_access_connector" "connector" { + count = var.vpc_connector_create != null ? 1 : 0 + project = local.project_id + name = ( + var.vpc_connector_create.name != null + ? var.vpc_connector_create.name + : var.name + ) + region = local.location + ip_cidr_range = var.vpc_connector_create.ip_cidr_range == null ? null : lookup( + local.ctx.cidr_ranges, var.vpc_connector_create.ip_cidr_range, + var.vpc_connector_create.ip_cidr_range + ) + network = var.vpc_connector_create.network == null ? null : lookup( + local.ctx.networks, var.vpc_connector_create.network, + var.vpc_connector_create.network + ) + machine_type = var.vpc_connector_create.machine_type + max_instances = var.vpc_connector_create.instances.max + max_throughput = var.vpc_connector_create.throughput.max + min_instances = var.vpc_connector_create.instances.min + min_throughput = var.vpc_connector_create.throughput.min + dynamic "subnet" { + for_each = var.vpc_connector_create.subnet.name == null ? [] : [""] + content { + name = local._connector_subnet_name + project_id = local._connector_subnet_project + } + } +} diff --git a/modules/cloud-function-v2/README.md b/modules/cloud-function-v2/README.md index 5617eaad0..9f60b645f 100644 --- a/modules/cloud-function-v2/README.md +++ b/modules/cloud-function-v2/README.md @@ -14,6 +14,7 @@ Cloud Function management, with support for IAM roles, optional bucket creation - [Private Cloud Build Pool](#private-cloud-build-pool) - [Multiple Cloud Functions within project](#multiple-cloud-functions-within-project) - [Mounting secrets from Secret Manager](#mounting-secrets-from-secret-manager) +- [VPC Access Connector](#vpc-access-connector) - [Variables](#variables) - [Outputs](#outputs) - [Fixtures](#fixtures) @@ -325,6 +326,85 @@ module "secret-manager" { } # tftest fixtures=fixtures/functions-default-sa-iam-grants.tf inventory=secrets.yaml e2e skip-tofu ``` + +## VPC Access Connector + +You can use an existing [VPC Access Connector](https://cloud.google.com/vpc/docs/serverless-vpc-access) to connect to a VPC from Cloud Run. + +```hcl +module "cf_http" { + source = "./fabric/modules/cloud-function-v2" + project_id = var.project_id + region = var.region + name = "test-cf-http" + bucket_name = var.bucket + bundle_config = { + path = "assets/sample-function/" + } + vpc_connector = { + name = google_vpc_access_connector.connector.id + egress_setting = "ALL_TRAFFIC" + } +} +# tftest fixtures=fixtures/vpc-connector.tf inventory=service-vpc-access-connector.yaml e2e +``` + +If creation of the VPC Access Connector is required, use the `vpc_connector.create` and `vpc_connector_create` variable which also supports optional attributes like number of instances, machine type, or throughput. + +```hcl +module "cf_http" { + source = "./fabric/modules/cloud-function-v2" + project_id = var.project_id + region = var.region + name = "test-cf-http" + bucket_name = var.bucket + bundle_config = { + path = "assets/sample-function/" + } + vpc_connector = { + create = true + } + vpc_connector_create = { + ip_cidr_range = "10.10.10.0/28" + network = var.vpc.self_link + instances = { + max = 10 + min = 3 + } + } +} +# tftest inventory=service-vpc-access-connector-create.yaml e2e +``` + +Note that if you are using a Shared VPC for the connector, you need to specify a subnet and the host project if this is not where the Cloud Run service is deployed. + +```hcl +module "cf_http" { + source = "./fabric/modules/cloud-function-v2" + project_id = var.project_id + region = var.region + name = "test-cf-http" + bucket_name = var.bucket + bundle_config = { + path = "assets/sample-function/" + } + vpc_connector = { + create = true + } + vpc_connector_create = { + machine_type = "e2-standard-4" + subnet = { + name = module.net-vpc-host.subnets["${var.region}/fixture-subnet-28"].name + project_id = module.project-host.project_id + } + throughput = { + max = 300 + min = 200 + } + } +} +# tftest fixtures=fixtures/shared-vpc.tf inventory=service-vpc-access-connector-create-sharedvpc.yaml e2e +``` ## Variables @@ -352,8 +432,8 @@ module "secret-manager" { | [secrets](variables.tf#L192) | Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format. | map(object({…})) | | {} | | [service_account_config](variables-serviceaccount.tf#L17) | Service account configurations. | object({…}) | | {} | | [trigger_config](variables.tf#L204) | Function trigger configuration. Leave null for HTTP trigger. | object({…}) | | null | -| [vpc_connector](variables.tf#L222) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | {} | -| [vpc_connector_config](variables.tf#L233) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | +| [vpc_connector](variables-vpcconnector.tf#L17) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | {} | +| [vpc_connector_create](variables-vpcconnector.tf#L28) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | ## Outputs @@ -379,4 +459,6 @@ module "secret-manager" { - [cloudbuild-custom-pool.tf](../../tests/fixtures/cloudbuild-custom-pool.tf) - [functions-default-sa-iam-grants.tf](../../tests/fixtures/functions-default-sa-iam-grants.tf) - [pubsub.tf](../../tests/fixtures/pubsub.tf) +- [shared-vpc.tf](../../tests/fixtures/shared-vpc.tf) +- [vpc-connector.tf](../../tests/fixtures/vpc-connector.tf) diff --git a/modules/cloud-function-v2/main.tf b/modules/cloud-function-v2/main.tf index db964075f..7905158ca 100644 --- a/modules/cloud-function-v2/main.tf +++ b/modules/cloud-function-v2/main.tf @@ -41,33 +41,7 @@ locals { var.trigger_config.service_account_email, null ) - vpc_connector = ( - var.vpc_connector.name == null - ? null - : ( - var.vpc_connector.create == false - ? var.vpc_connector.name - : google_vpc_access_connector.connector[0].id - ) - ) -} - -resource "google_vpc_access_connector" "connector" { - count = var.vpc_connector.create == true ? 1 : 0 - project = local.project_id - name = var.vpc_connector.name - region = local.location - ip_cidr_range = lookup(local.ctx.cidr_ranges, - var.vpc_connector_config.ip_cidr_range, - var.vpc_connector_config.ip_cidr_range - ) - network = lookup(local.ctx.networks, - var.vpc_connector_config.network, var.vpc_connector_config.network - ) - max_instances = try(var.vpc_connector_config.instances.max, null) - min_instances = try(var.vpc_connector_config.instances.min, null) - max_throughput = try(var.vpc_connector_config.throughput.max, null) - min_throughput = try(var.vpc_connector_config.throughput.min, null) + vpc_connector = var.vpc_connector.create ? google_vpc_access_connector.connector[0].id : var.vpc_connector.name } resource "google_cloudfunctions2_function" "function" { @@ -168,33 +142,35 @@ resource "google_cloudfunctions2_function_iam_binding" "binding" { for_each = { for k, v in var.iam : k => v if k != "roles/run.invoker" } - project = var.project_id - location = google_cloudfunctions2_function.function.location + project = local.project_id + location = local.location cloud_function = google_cloudfunctions2_function.function.name - role = each.key - members = each.value + role = lookup(local.ctx.custom_roles, each.key, each.key) + members = [for member in each.value : lookup(local.ctx.iam_principals, member, member)] lifecycle { replace_triggered_by = [google_cloudfunctions2_function.function] } } +locals { + run_invoker_members = distinct(compact(concat( + !local.trigger_sa_create + ? [] + : ["serviceAccount:${local.trigger_sa_email}"], + lookup(var.iam, "roles/run.invoker", []), + ))) +} + resource "google_cloud_run_service_iam_binding" "invoker" { # cloud run resources are needed for invoker role to the underlying service count = ( lookup(var.iam, "roles/run.invoker", null) != null ) ? 1 : 0 - project = var.project_id - location = google_cloudfunctions2_function.function.location + project = local.project_id + location = local.location service = google_cloudfunctions2_function.function.name role = "roles/run.invoker" - members = distinct(compact(concat( - lookup(var.iam, "roles/run.invoker", []), - ( - !local.trigger_sa_create - ? [] - : ["serviceAccount:${local.trigger_sa_email}"] - ) - ))) + members = [for member in local.run_invoker_members : lookup(local.ctx.iam_principals, member, member)] lifecycle { replace_triggered_by = [google_cloudfunctions2_function.function] } @@ -207,8 +183,8 @@ resource "google_cloud_run_service_iam_member" "invoker" { lookup(var.iam, "roles/run.invoker", null) == null && local.trigger_sa_create ) ? 1 : 0 - project = var.project_id - location = google_cloudfunctions2_function.function.location + project = local.project_id + location = local.location service = google_cloudfunctions2_function.function.name role = "roles/run.invoker" member = "serviceAccount:${local.trigger_sa_email}" diff --git a/modules/cloud-function-v2/variables-vpcconnector.tf b/modules/cloud-function-v2/variables-vpcconnector.tf new file mode 100644 index 000000000..967ce6983 --- /dev/null +++ b/modules/cloud-function-v2/variables-vpcconnector.tf @@ -0,0 +1,59 @@ +/** + * 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 "vpc_connector" { + description = "VPC connector configuration. Set create to 'true' if a new connector needs to be created." + type = object({ + create = optional(bool, false) + name = optional(string) + egress_settings = optional(string) + }) + nullable = false + default = {} +} + +variable "vpc_connector_create" { + description = "VPC connector network configuration. Must be provided if new VPC connector is being created." + type = object({ + ip_cidr_range = optional(string) + machine_type = optional(string) + name = optional(string) + network = optional(string) + instances = optional(object({ + max = optional(number) + min = optional(number) + }), {} + ) + throughput = optional(object({ + max = optional(number) + min = optional(number) + }), {} + ) + subnet = optional(object({ + name = optional(string) + project_id = optional(string) + }), {}) + }) + default = null + validation { + condition = ( + var.vpc_connector.create == false || + try(var.vpc_connector_create.instances, null) != null || + try(var.vpc_connector_create.throughput, null) != null + ) + error_message = "VPC connector must specify either instances or throughput." + } +} diff --git a/modules/cloud-function-v2/variables.tf b/modules/cloud-function-v2/variables.tf index 3b992812c..0926f8fd5 100644 --- a/modules/cloud-function-v2/variables.tf +++ b/modules/cloud-function-v2/variables.tf @@ -218,39 +218,3 @@ variable "trigger_config" { }) default = null } - -variable "vpc_connector" { - description = "VPC connector configuration. Set create to 'true' if a new connector needs to be created." - type = object({ - create = optional(bool, false) - name = optional(string) - egress_settings = optional(string) - }) - nullable = false - default = {} -} - -variable "vpc_connector_config" { - description = "VPC connector network configuration. Must be provided if new VPC connector is being created." - type = object({ - ip_cidr_range = string - network = string - instances = optional(object({ - max = optional(number) - min = optional(number, 2) - })) - throughput = optional(object({ - max = optional(number, 300) - min = optional(number, 200) - })) - }) - default = null - validation { - condition = ( - var.vpc_connector_config == null || - try(var.vpc_connector_config.instances, null) != null || - try(var.vpc_connector_config.throughput, null) != null - ) - error_message = "VPC connector must specify either instances or throughput." - } -} diff --git a/modules/cloud-function-v2/vpcconnector.tf b/modules/cloud-function-v2/vpcconnector.tf new file mode 100644 index 000000000..4d01eada0 --- /dev/null +++ b/modules/cloud-function-v2/vpcconnector.tf @@ -0,0 +1,74 @@ +/** + * 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 { + _connector_subnet_name_ctx = ( + try(var.vpc_connector_create.subnet.name, null) == null ? false : + contains(keys(local.ctx.subnets), var.vpc_connector_create.subnet.name) + ) + # if you pass the subnet, you must pass only the name, not the whole id + _connector_subnet_name = ( + local._connector_subnet_name_ctx + ? provider::google::name_from_id(local.ctx.subnets[var.vpc_connector_create.subnet.name]) + : try(var.vpc_connector_create.subnet.name, null) + ) + # if project is not provided, but subnet is coming from context, use project from subnet id in context + # and avoid lookups using null project + _connector_subnet_project_input = try(var.vpc_connector_create.subnet.project_id, null) + _connector_subnet_project = ( + local._connector_subnet_project_input == null + ? ( + local._connector_subnet_name_ctx + ? provider::google::project_from_id(local.ctx.subnets[var.vpc_connector_create.subnet.name]) + : null + ) + : lookup( + local.ctx.project_ids, local._connector_subnet_project_input, + local._connector_subnet_project_input + ) + ) +} + +resource "google_vpc_access_connector" "connector" { + count = var.vpc_connector_create != null ? 1 : 0 + project = local.project_id + name = ( + var.vpc_connector_create.name != null + ? var.vpc_connector_create.name + : var.name + ) + region = local.location + ip_cidr_range = var.vpc_connector_create.ip_cidr_range == null ? null : lookup( + local.ctx.cidr_ranges, var.vpc_connector_create.ip_cidr_range, + var.vpc_connector_create.ip_cidr_range + ) + network = var.vpc_connector_create.network == null ? null : lookup( + local.ctx.networks, var.vpc_connector_create.network, + var.vpc_connector_create.network + ) + machine_type = var.vpc_connector_create.machine_type + max_instances = var.vpc_connector_create.instances.max + max_throughput = var.vpc_connector_create.throughput.max + min_instances = var.vpc_connector_create.instances.min + min_throughput = var.vpc_connector_create.throughput.min + dynamic "subnet" { + for_each = var.vpc_connector_create.subnet.name == null ? [] : [""] + content { + name = local._connector_subnet_name + project_id = local._connector_subnet_project + } + } +} diff --git a/modules/cloud-run-v2/README.md b/modules/cloud-run-v2/README.md index 96141de7b..f33a3a13f 100644 --- a/modules/cloud-run-v2/README.md +++ b/modules/cloud-run-v2/README.md @@ -189,7 +189,7 @@ You can use an existing [VPC Access Connector](https://cloud.google.com/vpc/docs module "cloud_run" { source = "./fabric/modules/cloud-run-v2" project_id = var.project_id - region = var.region + region = var.regions.secondary name = "hello" containers = { hello = { diff --git a/tests/fixtures/vpc-connector.tf b/tests/fixtures/vpc-connector.tf index 483cf64d0..fda673012 100644 --- a/tests/fixtures/vpc-connector.tf +++ b/tests/fixtures/vpc-connector.tf @@ -17,7 +17,7 @@ resource "google_vpc_access_connector" "connector" { project = var.project_id name = "vpc-connector" - region = var.region + region = var.regions.secondary min_instances = 2 max_instances = 3 ip_cidr_range = "192.168.0.0/28" diff --git a/tests/modules/cloud_function_v1/context-subnet-project.tfvars b/tests/modules/cloud_function_v1/context-subnet-project.tfvars new file mode 100644 index 000000000..6428c97e1 --- /dev/null +++ b/tests/modules/cloud_function_v1/context-subnet-project.tfvars @@ -0,0 +1,58 @@ +name = "test-cf-kms" +bucket_name = "bucket" +bundle_config = { + path = "gs://assets/sample-function.zip" +} +context = { + cidr_ranges = { + test = "10.10.20.0/28" + } + custom_roles = { + myrole_one = "organizations/366118655033/roles/myRoleOne" + } + iam_principals = { + mygroup = "group:test-group@example.com" + } + kms_keys = { + test = "projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute" + } + locations = { + ew8 = "europe-west8" + } + networks = { + test = "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0" + } + project_ids = { + test = "foo-test-0" + } + subnets = { + test = "projects/foo-dev-net-spoke-0/regions/europe-west1/subnetworks/gce" + } +} +kms_key = "$kms_keys:test" +iam = { + "$custom_roles:myrole_one" = [ + "$iam_principals:mygroup" + ] +} +project_id = "$project_ids:test" +region = "$locations:ew8" +service_account_config = { + roles = [ + "$custom_roles:myrole_one" + ] +} +vpc_connector = { + create = true + name = "connector_name" +} +vpc_connector_create = { + instances = { + max = 10 + min = 3 + } + subnet = { + name = "$subnets:test" + project_id = "$project_ids:test" + } +} diff --git a/tests/modules/cloud_function_v1/context-subnet-project.yaml b/tests/modules/cloud_function_v1/context-subnet-project.yaml new file mode 100644 index 000000000..6ada43545 --- /dev/null +++ b/tests/modules/cloud_function_v1/context-subnet-project.yaml @@ -0,0 +1,61 @@ +# 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. + +values: + google_cloudfunctions_function.function: + kms_key_name: projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute + project: foo-test-0 + region: europe-west8 + service_account_email: test-cf-kms@foo-test-0.iam.gserviceaccount.com + google_cloudfunctions_function_iam_binding.default["$custom_roles:myrole_one"]: + condition: [] + members: + - group:test-group@example.com + project: foo-test-0 + region: europe-west8 + role: organizations/366118655033/roles/myRoleOne + google_project_iam_member.default["organizations/366118655033/roles/myRoleOne"]: + condition: [] + member: serviceAccount:test-cf-kms@foo-test-0.iam.gserviceaccount.com + project: foo-test-0 + role: organizations/366118655033/roles/myRoleOne + google_service_account.service_account[0]: + account_id: test-cf-kms + create_ignore_already_exists: null + description: null + disabled: false + display_name: test-cf-kms + email: test-cf-kms@foo-test-0.iam.gserviceaccount.com + member: serviceAccount:test-cf-kms@foo-test-0.iam.gserviceaccount.com + project: foo-test-0 + timeouts: null + google_vpc_access_connector.connector[0]: + ip_cidr_range: null + machine_type: e2-micro + max_instances: 10 + min_instances: 3 + name: test-cf-kms + project: foo-test-0 + region: europe-west8 + subnet: + - name: gce + project_id: foo-test-0 + timeouts: null + +counts: + google_cloudfunctions_function: 1 + google_cloudfunctions_function_iam_binding: 1 + google_project_iam_member: 1 + google_service_account: 1 + google_vpc_access_connector: 1 diff --git a/tests/modules/cloud_function_v1/context-subnet.tfvars b/tests/modules/cloud_function_v1/context-subnet.tfvars new file mode 100644 index 000000000..7512e3ab3 --- /dev/null +++ b/tests/modules/cloud_function_v1/context-subnet.tfvars @@ -0,0 +1,57 @@ +name = "test-cf-kms" +bucket_name = "bucket" +bundle_config = { + path = "gs://assets/sample-function.zip" +} +context = { + cidr_ranges = { + test = "10.10.20.0/28" + } + custom_roles = { + myrole_one = "organizations/366118655033/roles/myRoleOne" + } + iam_principals = { + mygroup = "group:test-group@example.com" + } + kms_keys = { + test = "projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute" + } + locations = { + ew8 = "europe-west8" + } + networks = { + test = "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0" + } + project_ids = { + test = "foo-test-0" + } + subnets = { + test = "projects/foo-dev-net-spoke-0/regions/europe-west1/subnetworks/gce" + } +} +kms_key = "$kms_keys:test" +iam = { + "$custom_roles:myrole_one" = [ + "$iam_principals:mygroup" + ] +} +project_id = "$project_ids:test" +region = "$locations:ew8" +service_account_config = { + roles = [ + "$custom_roles:myrole_one" + ] +} +vpc_connector = { + create = true + name = "connector_name" +} +vpc_connector_create = { + instances = { + max = 10 + min = 3 + } + subnet = { + name = "$subnets:test" + } +} diff --git a/tests/modules/cloud_function_v1/context-subnet.yaml b/tests/modules/cloud_function_v1/context-subnet.yaml new file mode 100644 index 000000000..de5cf036a --- /dev/null +++ b/tests/modules/cloud_function_v1/context-subnet.yaml @@ -0,0 +1,60 @@ +# 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. + +values: + google_cloudfunctions_function.function: + kms_key_name: projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute + project: foo-test-0 + region: europe-west8 + service_account_email: test-cf-kms@foo-test-0.iam.gserviceaccount.com + google_cloudfunctions_function_iam_binding.default["$custom_roles:myrole_one"]: + condition: [] + members: + - group:test-group@example.com + project: foo-test-0 + region: europe-west8 + role: organizations/366118655033/roles/myRoleOne + google_project_iam_member.default["organizations/366118655033/roles/myRoleOne"]: + condition: [] + member: serviceAccount:test-cf-kms@foo-test-0.iam.gserviceaccount.com + project: foo-test-0 + role: organizations/366118655033/roles/myRoleOne + google_service_account.service_account[0]: + account_id: test-cf-kms + create_ignore_already_exists: null + description: null + disabled: false + display_name: test-cf-kms + email: test-cf-kms@foo-test-0.iam.gserviceaccount.com + member: serviceAccount:test-cf-kms@foo-test-0.iam.gserviceaccount.com + project: foo-test-0 + timeouts: null + google_vpc_access_connector.connector[0]: + ip_cidr_range: null + machine_type: e2-micro + max_instances: 10 + min_instances: 3 + name: test-cf-kms + project: foo-test-0 + region: europe-west8 + subnet: + - name: gce + project_id: foo-dev-net-spoke-0 + +counts: + google_cloudfunctions_function: 1 + google_cloudfunctions_function_iam_binding: 1 + google_project_iam_member: 1 + google_service_account: 1 + google_vpc_access_connector: 1 diff --git a/tests/modules/cloud_function_v1/context.tfvars b/tests/modules/cloud_function_v1/context.tfvars index 558271b35..f6eb5e3e3 100644 --- a/tests/modules/cloud_function_v1/context.tfvars +++ b/tests/modules/cloud_function_v1/context.tfvars @@ -46,7 +46,7 @@ vpc_connector = { create = true name = "connector_name" } -vpc_connector_config = { +vpc_connector_create = { ip_cidr_range = "$cidr_ranges:test" network = "$networks:test" instances = { diff --git a/tests/modules/cloud_function_v1/context.yaml b/tests/modules/cloud_function_v1/context.yaml index 6417d4548..da60e5571 100644 --- a/tests/modules/cloud_function_v1/context.yaml +++ b/tests/modules/cloud_function_v1/context.yaml @@ -45,7 +45,7 @@ values: machine_type: e2-micro max_instances: 10 min_instances: 3 - name: connector_name + name: test-cf-kms network: projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0 project: foo-test-0 region: europe-west8 diff --git a/tests/modules/cloud_function_v1/examples/service-vpc-access-connector-create-sharedvpc.yaml b/tests/modules/cloud_function_v1/examples/service-vpc-access-connector-create-sharedvpc.yaml new file mode 100644 index 000000000..1ae759ba3 --- /dev/null +++ b/tests/modules/cloud_function_v1/examples/service-vpc-access-connector-create-sharedvpc.yaml @@ -0,0 +1,68 @@ +# 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. + +values: + module.cf_http.google_cloudfunctions_function.function: + available_memory_mb: 256 + build_environment_variables: null + build_worker_pool: null + description: Terraform managed. + docker_registry: ARTIFACT_REGISTRY + docker_repository: null + effective_labels: + goog-terraform-provisioned: 'true' + entry_point: main + environment_variables: null + https_trigger_security_level: SECURE_ALWAYS + ingress_settings: ALLOW_ALL + kms_key_name: null + labels: null + max_instances: 1 + min_instances: null + name: test-cf-http + on_deploy_update_policy: [] + project: project-id + region: europe-west8 + runtime: python310 + secret_environment_variables: [] + secret_volumes: [] + service_account_email: test-cf-http@project-id.iam.gserviceaccount.com + source_archive_bucket: bucket + source_archive_object: bundle-95c1b0e5b92dae8333539b1e0ad5173b.zip + source_repository: [] + terraform_labels: + goog-terraform-provisioned: 'true' + timeout: 180 + timeouts: null + trigger_http: true + module.cf_http.google_vpc_access_connector.connector[0]: + ip_cidr_range: null + machine_type: e2-standard-4 + max_throughput: 300 + min_throughput: 200 + name: test-cf-http + project: project-id + region: europe-west8 + subnet: + - name: fixture-subnet-28 + project_id: test-host + timeouts: null + +counts: + google_cloudfunctions_function: 1 + google_service_account: 1 + google_storage_bucket_object: 1 + + +outputs: {} diff --git a/tests/modules/cloud_function_v1/examples/service-vpc-access-connector-create.yaml b/tests/modules/cloud_function_v1/examples/service-vpc-access-connector-create.yaml new file mode 100644 index 000000000..e1c215525 --- /dev/null +++ b/tests/modules/cloud_function_v1/examples/service-vpc-access-connector-create.yaml @@ -0,0 +1,56 @@ +# 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. + +values: + module.cf_http.google_cloudfunctions_function.function: + available_memory_mb: 256 + build_environment_variables: null + build_worker_pool: null + description: Terraform managed. + docker_registry: ARTIFACT_REGISTRY + docker_repository: null + effective_labels: + goog-terraform-provisioned: 'true' + entry_point: main + environment_variables: null + https_trigger_security_level: SECURE_ALWAYS + ingress_settings: ALLOW_ALL + kms_key_name: null + labels: null + max_instances: 1 + min_instances: null + name: test-cf-http + on_deploy_update_policy: [] + project: project-id + region: europe-west8 + runtime: python310 + secret_environment_variables: [] + secret_volumes: [] + service_account_email: test-cf-http@project-id.iam.gserviceaccount.com + source_archive_bucket: bucket + source_repository: [] + terraform_labels: + goog-terraform-provisioned: 'true' + timeout: 180 + timeouts: null + trigger_http: true + +counts: + google_cloudfunctions_function: 1 + google_project_iam_member: 2 + google_service_account: 1 + google_storage_bucket_object: 1 + + +outputs: {} diff --git a/tests/modules/cloud_function_v1/examples/service-vpc-access-connector.yaml b/tests/modules/cloud_function_v1/examples/service-vpc-access-connector.yaml new file mode 100644 index 000000000..e1c215525 --- /dev/null +++ b/tests/modules/cloud_function_v1/examples/service-vpc-access-connector.yaml @@ -0,0 +1,56 @@ +# 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. + +values: + module.cf_http.google_cloudfunctions_function.function: + available_memory_mb: 256 + build_environment_variables: null + build_worker_pool: null + description: Terraform managed. + docker_registry: ARTIFACT_REGISTRY + docker_repository: null + effective_labels: + goog-terraform-provisioned: 'true' + entry_point: main + environment_variables: null + https_trigger_security_level: SECURE_ALWAYS + ingress_settings: ALLOW_ALL + kms_key_name: null + labels: null + max_instances: 1 + min_instances: null + name: test-cf-http + on_deploy_update_policy: [] + project: project-id + region: europe-west8 + runtime: python310 + secret_environment_variables: [] + secret_volumes: [] + service_account_email: test-cf-http@project-id.iam.gserviceaccount.com + source_archive_bucket: bucket + source_repository: [] + terraform_labels: + goog-terraform-provisioned: 'true' + timeout: 180 + timeouts: null + trigger_http: true + +counts: + google_cloudfunctions_function: 1 + google_project_iam_member: 2 + google_service_account: 1 + google_storage_bucket_object: 1 + + +outputs: {} diff --git a/tests/modules/cloud_function_v1/tftest.yaml b/tests/modules/cloud_function_v1/tftest.yaml index ea15fe700..f763f77be 100644 --- a/tests/modules/cloud_function_v1/tftest.yaml +++ b/tests/modules/cloud_function_v1/tftest.yaml @@ -15,4 +15,6 @@ module: modules/cloud-function-v1 tests: context: + context-subnet: + context-subnet-project: kms: diff --git a/tests/modules/cloud_function_v2/context-subnet-project.tfvars b/tests/modules/cloud_function_v2/context-subnet-project.tfvars new file mode 100644 index 000000000..6428c97e1 --- /dev/null +++ b/tests/modules/cloud_function_v2/context-subnet-project.tfvars @@ -0,0 +1,58 @@ +name = "test-cf-kms" +bucket_name = "bucket" +bundle_config = { + path = "gs://assets/sample-function.zip" +} +context = { + cidr_ranges = { + test = "10.10.20.0/28" + } + custom_roles = { + myrole_one = "organizations/366118655033/roles/myRoleOne" + } + iam_principals = { + mygroup = "group:test-group@example.com" + } + kms_keys = { + test = "projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute" + } + locations = { + ew8 = "europe-west8" + } + networks = { + test = "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0" + } + project_ids = { + test = "foo-test-0" + } + subnets = { + test = "projects/foo-dev-net-spoke-0/regions/europe-west1/subnetworks/gce" + } +} +kms_key = "$kms_keys:test" +iam = { + "$custom_roles:myrole_one" = [ + "$iam_principals:mygroup" + ] +} +project_id = "$project_ids:test" +region = "$locations:ew8" +service_account_config = { + roles = [ + "$custom_roles:myrole_one" + ] +} +vpc_connector = { + create = true + name = "connector_name" +} +vpc_connector_create = { + instances = { + max = 10 + min = 3 + } + subnet = { + name = "$subnets:test" + project_id = "$project_ids:test" + } +} diff --git a/tests/modules/cloud_function_v2/context-subnet-project.yaml b/tests/modules/cloud_function_v2/context-subnet-project.yaml new file mode 100644 index 000000000..1c0947fb9 --- /dev/null +++ b/tests/modules/cloud_function_v2/context-subnet-project.yaml @@ -0,0 +1,64 @@ +# 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. + +values: + google_cloudfunctions2_function.function: + kms_key_name: projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute + location: europe-west8 + name: test-cf-kms + project: foo-test-0 + service_config: + - service_account_email: test-cf-kms@foo-test-0.iam.gserviceaccount.com + google_cloudfunctions2_function_iam_binding.binding["$custom_roles:myrole_one"]: + cloud_function: test-cf-kms + condition: [] + members: + - group:test-group@example.com + project: foo-test-0 + location: europe-west8 + role: organizations/366118655033/roles/myRoleOne + google_project_iam_member.default["organizations/366118655033/roles/myRoleOne"]: + condition: [] + member: serviceAccount:test-cf-kms@foo-test-0.iam.gserviceaccount.com + project: foo-test-0 + role: organizations/366118655033/roles/myRoleOne + google_service_account.service_account[0]: + account_id: test-cf-kms + create_ignore_already_exists: null + description: null + disabled: false + display_name: test-cf-kms + email: test-cf-kms@foo-test-0.iam.gserviceaccount.com + member: serviceAccount:test-cf-kms@foo-test-0.iam.gserviceaccount.com + project: foo-test-0 + timeouts: null + google_vpc_access_connector.connector[0]: + ip_cidr_range: null + machine_type: e2-micro + max_instances: 10 + min_instances: 3 + name: test-cf-kms + project: foo-test-0 + region: europe-west8 + subnet: + - name: gce + project_id: foo-test-0 + timeouts: null + +counts: + google_cloudfunctions2_function: 1 + google_cloudfunctions2_function_iam_binding: 1 + google_project_iam_member: 1 + google_service_account: 1 + google_vpc_access_connector: 1 diff --git a/tests/modules/cloud_function_v2/context-subnet.tfvars b/tests/modules/cloud_function_v2/context-subnet.tfvars new file mode 100644 index 000000000..7512e3ab3 --- /dev/null +++ b/tests/modules/cloud_function_v2/context-subnet.tfvars @@ -0,0 +1,57 @@ +name = "test-cf-kms" +bucket_name = "bucket" +bundle_config = { + path = "gs://assets/sample-function.zip" +} +context = { + cidr_ranges = { + test = "10.10.20.0/28" + } + custom_roles = { + myrole_one = "organizations/366118655033/roles/myRoleOne" + } + iam_principals = { + mygroup = "group:test-group@example.com" + } + kms_keys = { + test = "projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute" + } + locations = { + ew8 = "europe-west8" + } + networks = { + test = "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0" + } + project_ids = { + test = "foo-test-0" + } + subnets = { + test = "projects/foo-dev-net-spoke-0/regions/europe-west1/subnetworks/gce" + } +} +kms_key = "$kms_keys:test" +iam = { + "$custom_roles:myrole_one" = [ + "$iam_principals:mygroup" + ] +} +project_id = "$project_ids:test" +region = "$locations:ew8" +service_account_config = { + roles = [ + "$custom_roles:myrole_one" + ] +} +vpc_connector = { + create = true + name = "connector_name" +} +vpc_connector_create = { + instances = { + max = 10 + min = 3 + } + subnet = { + name = "$subnets:test" + } +} diff --git a/tests/modules/cloud_function_v2/context-subnet.yaml b/tests/modules/cloud_function_v2/context-subnet.yaml new file mode 100644 index 000000000..de83bf86d --- /dev/null +++ b/tests/modules/cloud_function_v2/context-subnet.yaml @@ -0,0 +1,62 @@ +# 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. + +values: + google_cloudfunctions2_function.function: + kms_key_name: projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute + location: europe-west8 + project: foo-test-0 + service_config: + - service_account_email: test-cf-kms@foo-test-0.iam.gserviceaccount.com + google_cloudfunctions2_function_iam_binding.binding["$custom_roles:myrole_one"]: + cloud_function: test-cf-kms + condition: [] + members: + - group:test-group@example.com + project: foo-test-0 + location: europe-west8 + role: organizations/366118655033/roles/myRoleOne + google_project_iam_member.default["organizations/366118655033/roles/myRoleOne"]: + condition: [] + member: serviceAccount:test-cf-kms@foo-test-0.iam.gserviceaccount.com + project: foo-test-0 + role: organizations/366118655033/roles/myRoleOne + google_service_account.service_account[0]: + account_id: test-cf-kms + create_ignore_already_exists: null + description: null + disabled: false + display_name: test-cf-kms + email: test-cf-kms@foo-test-0.iam.gserviceaccount.com + member: serviceAccount:test-cf-kms@foo-test-0.iam.gserviceaccount.com + project: foo-test-0 + timeouts: null + google_vpc_access_connector.connector[0]: + ip_cidr_range: null + machine_type: e2-micro + max_instances: 10 + min_instances: 3 + name: test-cf-kms + project: foo-test-0 + region: europe-west8 + subnet: + - name: gce + project_id: foo-dev-net-spoke-0 + +counts: + google_cloudfunctions2_function: 1 + google_cloudfunctions2_function_iam_binding: 1 + google_project_iam_member: 1 + google_service_account: 1 + google_vpc_access_connector: 1 diff --git a/tests/modules/cloud_function_v2/context.tfvars b/tests/modules/cloud_function_v2/context.tfvars new file mode 100644 index 000000000..f6eb5e3e3 --- /dev/null +++ b/tests/modules/cloud_function_v2/context.tfvars @@ -0,0 +1,56 @@ +name = "test-cf-kms" +bucket_name = "bucket" +bundle_config = { + path = "gs://assets/sample-function.zip" +} +context = { + cidr_ranges = { + test = "10.10.20.0/28" + } + custom_roles = { + myrole_one = "organizations/366118655033/roles/myRoleOne" + } + iam_principals = { + mygroup = "group:test-group@example.com" + } + kms_keys = { + test = "projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute" + } + locations = { + ew8 = "europe-west8" + } + networks = { + test = "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0" + } + project_ids = { + test = "foo-test-0" + } + subnets = { + test = "projects/foo-dev-net-spoke-0/regions/europe-west1/subnetworks/gce" + } +} +kms_key = "$kms_keys:test" +iam = { + "$custom_roles:myrole_one" = [ + "$iam_principals:mygroup" + ] +} +project_id = "$project_ids:test" +region = "$locations:ew8" +service_account_config = { + roles = [ + "$custom_roles:myrole_one" + ] +} +vpc_connector = { + create = true + name = "connector_name" +} +vpc_connector_create = { + ip_cidr_range = "$cidr_ranges:test" + network = "$networks:test" + instances = { + max = 10 + min = 3 + } +} diff --git a/tests/modules/cloud_function_v2/context.yaml b/tests/modules/cloud_function_v2/context.yaml new file mode 100644 index 000000000..4f9a08057 --- /dev/null +++ b/tests/modules/cloud_function_v2/context.yaml @@ -0,0 +1,61 @@ +# 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. + +values: + google_cloudfunctions2_function.function: + kms_key_name: projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute + location: europe-west8 + project: foo-test-0 + service_config: + - service_account_email: test-cf-kms@foo-test-0.iam.gserviceaccount.com + google_cloudfunctions2_function_iam_binding.binding["$custom_roles:myrole_one"]: + condition: [] + members: + - group:test-group@example.com + project: foo-test-0 + location: europe-west8 + role: organizations/366118655033/roles/myRoleOne + google_project_iam_member.default["organizations/366118655033/roles/myRoleOne"]: + condition: [] + member: serviceAccount:test-cf-kms@foo-test-0.iam.gserviceaccount.com + project: foo-test-0 + role: organizations/366118655033/roles/myRoleOne + google_service_account.service_account[0]: + account_id: test-cf-kms + create_ignore_already_exists: null + description: null + disabled: false + display_name: test-cf-kms + email: test-cf-kms@foo-test-0.iam.gserviceaccount.com + member: serviceAccount:test-cf-kms@foo-test-0.iam.gserviceaccount.com + project: foo-test-0 + timeouts: null + google_vpc_access_connector.connector[0]: + ip_cidr_range: 10.10.20.0/28 + machine_type: e2-micro + max_instances: 10 + min_instances: 3 + name: test-cf-kms + network: projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0 + project: foo-test-0 + region: europe-west8 + subnet: [] + timeouts: null + +counts: + google_cloudfunctions2_function: 1 + google_cloudfunctions2_function_iam_binding: 1 + google_project_iam_member: 1 + google_service_account: 1 + google_vpc_access_connector: 1 diff --git a/tests/modules/cloud_function_v2/examples/service-vpc-access-connector-create-sharedvpc.yaml b/tests/modules/cloud_function_v2/examples/service-vpc-access-connector-create-sharedvpc.yaml new file mode 100644 index 000000000..e3cef2cec --- /dev/null +++ b/tests/modules/cloud_function_v2/examples/service-vpc-access-connector-create-sharedvpc.yaml @@ -0,0 +1,73 @@ +# 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. + +values: + module.cf_http.google_cloudfunctions2_function.function: + build_config: + - entry_point: main + on_deploy_update_policy: [] + runtime: python310 + source: + - repo_source: [] + storage_source: + - bucket: bucket + object: bundle-95c1b0e5b92dae8333539b1e0ad5173b.zip + worker_pool: null + description: Terraform managed. + effective_labels: + goog-terraform-provisioned: 'true' + event_trigger: [] + kms_key_name: null + labels: null + location: europe-west8 + name: test-cf-http + project: project-id + service_config: + - all_traffic_on_latest_revision: true + available_cpu: '0.166' + available_memory: 256M + binary_authorization_policy: null + environment_variables: + LOG_EXECUTION_ID: 'true' + ingress_settings: ALLOW_ALL + max_instance_count: 1 + min_instance_count: 0 + secret_environment_variables: [] + secret_volumes: [] + service_account_email: test-cf-http@project-id.iam.gserviceaccount.com + timeout_seconds: 180 + vpc_connector_egress_settings: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + module.cf_http.google_vpc_access_connector.connector[0]: + ip_cidr_range: null + machine_type: e2-standard-4 + max_throughput: 300 + min_throughput: 200 + name: test-cf-http + project: project-id + region: europe-west8 + subnet: + - name: fixture-subnet-28 + project_id: test-host + timeouts: null + +counts: + google_cloudfunctions2_function: 1 + google_service_account: 1 + google_storage_bucket_object: 1 + + +outputs: {} diff --git a/tests/modules/cloud_function_v2/examples/service-vpc-access-connector-create.yaml b/tests/modules/cloud_function_v2/examples/service-vpc-access-connector-create.yaml new file mode 100644 index 000000000..ba62ca04a --- /dev/null +++ b/tests/modules/cloud_function_v2/examples/service-vpc-access-connector-create.yaml @@ -0,0 +1,62 @@ +# 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. + +values: + module.cf_http.google_cloudfunctions2_function.function: + build_config: + - entry_point: main + on_deploy_update_policy: [] + runtime: python310 + source: + - repo_source: [] + storage_source: + - bucket: bucket + object: bundle-95c1b0e5b92dae8333539b1e0ad5173b.zip + worker_pool: null + description: Terraform managed. + effective_labels: + goog-terraform-provisioned: 'true' + event_trigger: [] + kms_key_name: null + labels: null + location: europe-west8 + name: test-cf-http + project: project-id + service_config: + - all_traffic_on_latest_revision: true + available_cpu: '0.166' + available_memory: 256M + binary_authorization_policy: null + environment_variables: + LOG_EXECUTION_ID: 'true' + ingress_settings: ALLOW_ALL + max_instance_count: 1 + min_instance_count: 0 + secret_environment_variables: [] + secret_volumes: [] + service_account_email: test-cf-http@project-id.iam.gserviceaccount.com + timeout_seconds: 180 + vpc_connector_egress_settings: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + +counts: + google_cloudfunctions2_function: 1 + google_project_iam_member: 2 + google_service_account: 1 + google_storage_bucket_object: 1 + + +outputs: {} diff --git a/tests/modules/cloud_function_v2/examples/service-vpc-access-connector.yaml b/tests/modules/cloud_function_v2/examples/service-vpc-access-connector.yaml new file mode 100644 index 000000000..ba62ca04a --- /dev/null +++ b/tests/modules/cloud_function_v2/examples/service-vpc-access-connector.yaml @@ -0,0 +1,62 @@ +# 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. + +values: + module.cf_http.google_cloudfunctions2_function.function: + build_config: + - entry_point: main + on_deploy_update_policy: [] + runtime: python310 + source: + - repo_source: [] + storage_source: + - bucket: bucket + object: bundle-95c1b0e5b92dae8333539b1e0ad5173b.zip + worker_pool: null + description: Terraform managed. + effective_labels: + goog-terraform-provisioned: 'true' + event_trigger: [] + kms_key_name: null + labels: null + location: europe-west8 + name: test-cf-http + project: project-id + service_config: + - all_traffic_on_latest_revision: true + available_cpu: '0.166' + available_memory: 256M + binary_authorization_policy: null + environment_variables: + LOG_EXECUTION_ID: 'true' + ingress_settings: ALLOW_ALL + max_instance_count: 1 + min_instance_count: 0 + secret_environment_variables: [] + secret_volumes: [] + service_account_email: test-cf-http@project-id.iam.gserviceaccount.com + timeout_seconds: 180 + vpc_connector_egress_settings: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + +counts: + google_cloudfunctions2_function: 1 + google_project_iam_member: 2 + google_service_account: 1 + google_storage_bucket_object: 1 + + +outputs: {} diff --git a/tests/modules/cloud_function_v2/kms.tfvars b/tests/modules/cloud_function_v2/kms.tfvars new file mode 100644 index 000000000..2fbed27c2 --- /dev/null +++ b/tests/modules/cloud_function_v2/kms.tfvars @@ -0,0 +1,9 @@ +project_id = "project" +region = "region" +name = "test-cf-kms" +bucket_name = "bucket" +bundle_config = { + path = "gs://assets/sample-function.zip" +} +kms_key = "kms_key_id" +docker_repository_id = "artifact_registry_id" diff --git a/tests/modules/cloud_function_v2/kms.yaml b/tests/modules/cloud_function_v2/kms.yaml new file mode 100644 index 000000000..882ac992a --- /dev/null +++ b/tests/modules/cloud_function_v2/kms.yaml @@ -0,0 +1,44 @@ +# 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. + +values: + google_cloudfunctions2_function.function: + build_config: + - docker_repository: artifact_registry_id + source: + - repo_source: [] + storage_source: + - bucket: bucket + object: sample-function.zip + kms_key_name: kms_key_id + name: test-cf-kms + project: project + location: region + service_config: + - service_account_email: test-cf-kms@project.iam.gserviceaccount.com + +counts: + google_cloudfunctions2_function: 1 + +outputs: + bucket: null + bucket_name: bucket + function: __missing__ + function_name: test-cf-kms + id: __missing__ + invoke_command: __missing__ + service_account: __missing__ + service_account_email: test-cf-kms@project.iam.gserviceaccount.com + service_account_iam_email: serviceAccount:test-cf-kms@project.iam.gserviceaccount.com + vpc_connector: null diff --git a/tests/modules/cloud_function_v2/tftest.yaml b/tests/modules/cloud_function_v2/tftest.yaml new file mode 100644 index 000000000..1ed90ec39 --- /dev/null +++ b/tests/modules/cloud_function_v2/tftest.yaml @@ -0,0 +1,21 @@ +# 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. + +module: modules/cloud-function-v2 +tests: + context: + context-subnet: + context-subnet-project: + kms: + vpcconnector: diff --git a/tests/modules/cloud_function_v2/vpcconnector.tfvars b/tests/modules/cloud_function_v2/vpcconnector.tfvars new file mode 100644 index 000000000..2df2acf7b --- /dev/null +++ b/tests/modules/cloud_function_v2/vpcconnector.tfvars @@ -0,0 +1,11 @@ +project_id = "test-project" +region = "region" +name = "test-cf-vpc" +bucket_name = "bucket" +bundle_config = { + path = "gs://assets/sample-function.zip" +} +vpc_connector = { + name = "projects/test-project/locations/region/connectors/vpc-connector" + egress_settings = "ALL_TRAFFIC" +} diff --git a/tests/modules/cloud_function_v2/vpcconnector.yaml b/tests/modules/cloud_function_v2/vpcconnector.yaml new file mode 100644 index 000000000..d66ace823 --- /dev/null +++ b/tests/modules/cloud_function_v2/vpcconnector.yaml @@ -0,0 +1,44 @@ +# 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. + +values: + google_cloudfunctions2_function.function: + build_config: + - source: + - repo_source: [] + storage_source: + - bucket: bucket + object: sample-function.zip + name: test-cf-vpc + project: test-project + location: region + service_config: + - service_account_email: test-cf-vpc@test-project.iam.gserviceaccount.com + vpc_connector: projects/test-project/locations/region/connectors/vpc-connector + vpc_connector_egress_settings: ALL_TRAFFIC + +counts: + google_cloudfunctions2_function: 1 + +outputs: + bucket: null + bucket_name: bucket + function: __missing__ + function_name: test-cf-vpc + id: __missing__ + invoke_command: __missing__ + service_account: __missing__ + service_account_email: test-cf-vpc@test-project.iam.gserviceaccount.com + service_account_iam_email: serviceAccount:test-cf-vpc@test-project.iam.gserviceaccount.com + vpc_connector: null diff --git a/tests/modules/cloud_run_v2/examples/service-vpc-access-connector.yaml b/tests/modules/cloud_run_v2/examples/service-vpc-access-connector.yaml index 4c94468c7..bee8ea643 100644 --- a/tests/modules/cloud_run_v2/examples/service-vpc-access-connector.yaml +++ b/tests/modules/cloud_run_v2/examples/service-vpc-access-connector.yaml @@ -14,7 +14,7 @@ values: module.cloud_run.google_cloud_run_v2_service.service[0]: - location: europe-west8 + location: europe-west9 name: hello project: project-id template: @@ -42,4 +42,3 @@ counts: resources: 2 outputs: {} - diff --git a/tools/duplicate-diff.py b/tools/duplicate-diff.py index c22c93b48..2628c0f9f 100755 --- a/tools/duplicate-diff.py +++ b/tools/duplicate-diff.py @@ -29,6 +29,10 @@ duplicates = [ # "fast/stages/2-networking-b-nva/data/cidrs.yaml", "fast/stages/2-networking-c-separate-envs/data/cidrs.yaml", ], + [ + "modules/cloud-function-v1/bundle.tf", + "modules/cloud-function-v2/bundle.tf", + ], [ "modules/cloud-function-v1/serviceaccount.tf", "modules/cloud-function-v2/serviceaccount.tf", @@ -38,9 +42,13 @@ duplicates = [ # "modules/cloud-function-v2/variables-serviceaccount.tf", ], [ - "modules/cloud-function-v1/bundle.tf", - "modules/cloud-function-v2/bundle.tf", - ] + "modules/cloud-function-v1/variables-vpcconnector.tf", + "modules/cloud-function-v2/variables-vpcconnector.tf", + ], + [ + "modules/cloud-function-v1/vpcconnector.tf", + "modules/cloud-function-v2/vpcconnector.tf", + ], ] From 8080192c3929afb490176b2fb9b0a411b1e803d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Tue, 21 Oct 2025 12:08:59 +0000 Subject: [PATCH 8/9] Dont use provider functions, because OpenTofu doesnt like them --- modules/cloud-function-v1/vpcconnector.tf | 4 ++-- modules/cloud-function-v2/vpcconnector.tf | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/cloud-function-v1/vpcconnector.tf b/modules/cloud-function-v1/vpcconnector.tf index 4d01eada0..2c449132a 100644 --- a/modules/cloud-function-v1/vpcconnector.tf +++ b/modules/cloud-function-v1/vpcconnector.tf @@ -22,7 +22,7 @@ locals { # if you pass the subnet, you must pass only the name, not the whole id _connector_subnet_name = ( local._connector_subnet_name_ctx - ? provider::google::name_from_id(local.ctx.subnets[var.vpc_connector_create.subnet.name]) + ? reverse(split("/", local.ctx.subnets[var.vpc_connector_create.subnet.name]))[0] : try(var.vpc_connector_create.subnet.name, null) ) # if project is not provided, but subnet is coming from context, use project from subnet id in context @@ -32,7 +32,7 @@ locals { local._connector_subnet_project_input == null ? ( local._connector_subnet_name_ctx - ? provider::google::project_from_id(local.ctx.subnets[var.vpc_connector_create.subnet.name]) + ? split("/", local.ctx.subnets[var.vpc_connector_create.subnet.name])[1] : null ) : lookup( diff --git a/modules/cloud-function-v2/vpcconnector.tf b/modules/cloud-function-v2/vpcconnector.tf index 4d01eada0..2c449132a 100644 --- a/modules/cloud-function-v2/vpcconnector.tf +++ b/modules/cloud-function-v2/vpcconnector.tf @@ -22,7 +22,7 @@ locals { # if you pass the subnet, you must pass only the name, not the whole id _connector_subnet_name = ( local._connector_subnet_name_ctx - ? provider::google::name_from_id(local.ctx.subnets[var.vpc_connector_create.subnet.name]) + ? reverse(split("/", local.ctx.subnets[var.vpc_connector_create.subnet.name]))[0] : try(var.vpc_connector_create.subnet.name, null) ) # if project is not provided, but subnet is coming from context, use project from subnet id in context @@ -32,7 +32,7 @@ locals { local._connector_subnet_project_input == null ? ( local._connector_subnet_name_ctx - ? provider::google::project_from_id(local.ctx.subnets[var.vpc_connector_create.subnet.name]) + ? split("/", local.ctx.subnets[var.vpc_connector_create.subnet.name])[1] : null ) : lookup( From 3a3142d8529e8ef39355a0a4cee3f2bba38552fa Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Thu, 23 Oct 2025 14:48:24 +0000 Subject: [PATCH 9/9] changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 099ed16dc..0f57791cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. ### BREAKING CHANGES +- `modules/cloud-function-v1`: service_account and service_account_create were moved to service_account_config. By default, module now creates a service accounts and grants `roles/logging.logWriter` and `roles/monitoring.metricWriter` on project level +`modules/cloud-function-v2`: service_account and service_account_create were moved to service_account_config. By default, module now creates a service accounts and grants `roles/logging.logWriter` and `roles/monitoring.metricWriter` on project level [[#3443](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3443)] - `terraform-provider-google`: Bump provider to 7.6.0, to allow use of `google_vertex_ai_reasoning_engine` in modules/agent-engine [[#3429](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3429)] - `modules/project-factory`: the `factories_config` attribute has been removed from project defaults and overrides. [[#3440](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3440)] - `modules/gke-hub`: Unified cluster configuration. The module now uses a single `clusters` variable to configure both cluster registration and feature enablement. [[#3332](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3332)] @@ -14,11 +16,15 @@ All notable changes to this project will be documented in this file. - `all modules`: Minimum supported OpenTofu version bumped 1.10.0 [[#3332](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3332)] - `modules/project-factory`: the format for automation service account names has changed. [[#3345](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3345)] +- [[#3456](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3456)] More knowledge - ([wiktorn](https://github.com/wiktorn)) +- [[#3455](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3455)] Service agents cursed knowledge ([juliocc](https://github.com/juliocc)) +- [[#3446](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3446)] Cursed knowledge of CFF ([wiktorn](https://github.com/wiktorn)) - [[#3428](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3428)] gitignore update ([juliocc](https://github.com/juliocc)) - [[#3361](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3361)] Use pre-commit managed Python environment for pre-commit checks ([wiktorn](https://github.com/wiktorn)) ### FAST +- [[#3443](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3443)] Cloud Function v1, v2 - contexts and service_account_config ([wiktorn](https://github.com/wiktorn)) - [[#3429](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3429)] Add Agent Engine module. ([LucaPrete](https://github.com/LucaPrete)) - [[#3440](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3440)] Support resource-level factories config in project factory module and FAST stages ([ludoo](https://github.com/ludoo)) - [[#3439](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3439)] compatiblity fix: Github CICD templates Terraform version bump to 1.12.2 ([ysolt](https://github.com/ysolt)) @@ -43,6 +49,8 @@ All notable changes to this project will be documented in this file. ### MODULES +- [[#3443](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3443)] Cloud Function v1, v2 - contexts and service_account_config ([wiktorn](https://github.com/wiktorn)) +- [[#3448](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3448)] Skip IAM grants for service agents that are not created on API activation ([juliocc](https://github.com/juliocc)) - [[#3445](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3445)] Remove Netsec Authz Service Agent ([juliocc](https://github.com/juliocc)) - [[#3429](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3429)] Add Agent Engine module. ([LucaPrete](https://github.com/LucaPrete)) - [[#3438](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3438)] Add PAM support ([juliocc](https://github.com/juliocc)) @@ -82,6 +90,8 @@ All notable changes to this project will be documented in this file. ### TOOLS +- [[#3443](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3443)] Cloud Function v1, v2 - contexts and service_account_config ([wiktorn](https://github.com/wiktorn)) +- [[#3448](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3448)] Skip IAM grants for service agents that are not created on API activation ([juliocc](https://github.com/juliocc)) - [[#3445](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3445)] Remove Netsec Authz Service Agent ([juliocc](https://github.com/juliocc)) - [[#3429](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3429)] Add Agent Engine module. ([LucaPrete](https://github.com/LucaPrete)) - [[#3444](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3444)] fix Terraform version linter ([wiktorn](https://github.com/wiktorn))