From 2830e4b9e203848080dca693d55142492284d97a Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Mon, 19 Jun 2023 12:50:36 +0200 Subject: [PATCH 1/5] Split Cloud Function module in separate v1 and v2 modules (#1450) * split v1 * v2 * blueprints * remove _http --- README.md | 2 +- blueprints/apigee/bigquery-analytics/main.tf | 20 +- .../asset-inventory-feed-remediation/main.tf | 8 +- .../deploy-cloud-function/main.tf | 8 +- .../cloud-operations/quota-monitoring/main.tf | 8 +- .../main.tf | 18 +- .../unmanaged-instances-healthcheck/main.tf | 14 +- .../main.tf | 2 +- blueprints/serverless/api-gateway/main.tf | 2 +- modules/README.md | 3 +- modules/cloud-function-v1/README.md | 236 ++++++++++++++++++ modules/cloud-function-v1/main.tf | 181 ++++++++++++++ modules/cloud-function-v1/outputs.tf | 65 +++++ .../variables.tf | 33 +-- .../versions.tf | 0 .../README.md | 89 ++----- .../main.tf | 129 +++------- .../outputs.tf | 8 +- modules/cloud-function-v2/variables.tf | 183 ++++++++++++++ .../cloud-function-v2/versions.tf | 18 +- tests/modules/cloud_function/common.tfvars | 11 - .../cloud_function/fixture/common.tfvars | 12 - tests/modules/cloud_function/fixture/main.tf | 31 --- .../cloud_function/fixture/variables.tf | 25 -- tests/modules/cloud_function/test_plan.py | 43 ---- .../cloud_function_v1/examples/iam.yaml | 28 +++ .../examples/multiple_functions.yaml | 0 .../cloud_function_v2/examples/iam.yaml | 29 +++ .../examples/multiple_functions.yaml} | 14 +- 29 files changed, 844 insertions(+), 376 deletions(-) create mode 100644 modules/cloud-function-v1/README.md create mode 100644 modules/cloud-function-v1/main.tf create mode 100644 modules/cloud-function-v1/outputs.tf rename modules/{cloud-function => cloud-function-v1}/variables.tf (84%) rename modules/{cloud-function => cloud-function-v1}/versions.tf (100%) rename modules/{cloud-function => cloud-function-v2}/README.md (77%) rename modules/{cloud-function => cloud-function-v2}/main.tf (59%) rename modules/{cloud-function => cloud-function-v2}/outputs.tf (89%) create mode 100644 modules/cloud-function-v2/variables.tf rename tests/modules/cloud_function/__init__.py => modules/cloud-function-v2/versions.tf (61%) delete mode 100644 tests/modules/cloud_function/common.tfvars delete mode 100644 tests/modules/cloud_function/fixture/common.tfvars delete mode 100644 tests/modules/cloud_function/fixture/main.tf delete mode 100644 tests/modules/cloud_function/fixture/variables.tf delete mode 100644 tests/modules/cloud_function/test_plan.py create mode 100644 tests/modules/cloud_function_v1/examples/iam.yaml rename tests/modules/{cloud_function => cloud_function_v1}/examples/multiple_functions.yaml (100%) create mode 100644 tests/modules/cloud_function_v2/examples/iam.yaml rename tests/modules/{cloud_function/bundle/main.py => cloud_function_v2/examples/multiple_functions.yaml} (59%) diff --git a/README.md b/README.md index 9573257fcf..6a64bc68db 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Currently available modules: - **data** - [AlloyDB instance](./modules/alloydb-instance), [BigQuery dataset](./modules/bigquery-dataset), [Bigtable instance](./modules/bigtable-instance), [Cloud Dataplex](./modules/cloud-dataplex), [Cloud SQL instance](./modules/cloudsql-instance), [Data Catalog Policy Tag](./modules/data-catalog-policy-tag), [Datafusion](./modules/datafusion), [Dataproc](./modules/dataproc), [GCS](./modules/gcs), [Pub/Sub](./modules/pubsub) - **development** - [API Gateway](./modules/api-gateway), [Apigee](./modules/apigee), [Artifact Registry](./modules/artifact-registry), [Container Registry](./modules/container-registry), [Cloud Source Repository](./modules/source-repository) - **security** - [Binauthz](./modules/binauthz/), [KMS](./modules/kms), [SecretManager](./modules/secret-manager), [VPC Service Control](./modules/vpc-sc) -- **serverless** - [Cloud Function](./modules/cloud-function), [Cloud Run](./modules/cloud-run) +- **serverless** - [Cloud Function v1](./modules/cloud-function-v1), [Cloud Function v2](./modules/cloud-function-v2), [Cloud Run](./modules/cloud-run) For more information and usage examples see each module's README file. diff --git a/blueprints/apigee/bigquery-analytics/main.tf b/blueprints/apigee/bigquery-analytics/main.tf index 1e1653c31f..97b42be8c7 100644 --- a/blueprints/apigee/bigquery-analytics/main.tf +++ b/blueprints/apigee/bigquery-analytics/main.tf @@ -152,7 +152,7 @@ module "bucket_export" { } module "function_export" { - source = "../../../modules/cloud-function" + source = "../../../modules/cloud-function-v1" project_id = module.project.project_id name = "export" bucket_name = "${module.project.project_id}-code-export" @@ -180,17 +180,15 @@ module "function_export" { DATASTORE = var.datastore_name } trigger_config = { - v1 = { - event = "google.pubsub.topic.publish" - resource = module.pubsub_export.id - retry = null - } + event = "google.pubsub.topic.publish" + resource = module.pubsub_export.id + retry = null } service_account_create = true } module "function_gcs2bq" { - source = "../../../modules/cloud-function" + source = "../../../modules/cloud-function-v1" project_id = module.project.project_id name = "gcs2bq" bucket_name = "${module.project.project_id}-code-gcs2bq" @@ -218,11 +216,9 @@ module "function_gcs2bq" { LOCATION = var.organization.analytics_region } trigger_config = { - v1 = { - event = "google.pubsub.topic.publish" - resource = module.bucket_export.topic - retry = null - } + event = "google.pubsub.topic.publish" + resource = module.bucket_export.topic + retry = null } service_account_create = true } diff --git a/blueprints/cloud-operations/asset-inventory-feed-remediation/main.tf b/blueprints/cloud-operations/asset-inventory-feed-remediation/main.tf index 163fc0f10c..e4082f69c3 100644 --- a/blueprints/cloud-operations/asset-inventory-feed-remediation/main.tf +++ b/blueprints/cloud-operations/asset-inventory-feed-remediation/main.tf @@ -74,7 +74,7 @@ module "service-account" { } module "cf" { - source = "../../../modules/cloud-function" + source = "../../../modules/cloud-function-v1" project_id = module.project.project_id name = var.name bucket_name = "${var.name}-${random_pet.random.id}" @@ -87,10 +87,8 @@ module "cf" { } service_account = module.service-account.email trigger_config = { - v1 = { - event = "google.pubsub.topic.publish" - resource = module.pubsub.topic.id - } + event = "google.pubsub.topic.publish" + resource = module.pubsub.topic.id } } diff --git a/blueprints/cloud-operations/network-dashboard/deploy-cloud-function/main.tf b/blueprints/cloud-operations/network-dashboard/deploy-cloud-function/main.tf index c3d22f6b90..a6da87ca1c 100644 --- a/blueprints/cloud-operations/network-dashboard/deploy-cloud-function/main.tf +++ b/blueprints/cloud-operations/network-dashboard/deploy-cloud-function/main.tf @@ -51,7 +51,7 @@ module "pubsub" { } module "cloud-function" { - source = "../../../../modules/cloud-function" + source = "../../../../modules/cloud-function-v1" project_id = module.project.project_id name = var.name bucket_name = coalesce( @@ -76,10 +76,8 @@ module "cloud-function" { } service_account_create = true trigger_config = { - v1 = { - event = "google.pubsub.topic.publish" - resource = module.pubsub.topic.id - } + event = "google.pubsub.topic.publish" + resource = module.pubsub.topic.id } vpc_connector = ( var.cloud_function_config.vpc_connector == null diff --git a/blueprints/cloud-operations/quota-monitoring/main.tf b/blueprints/cloud-operations/quota-monitoring/main.tf index 23aaf6f2d0..841bb803ee 100644 --- a/blueprints/cloud-operations/quota-monitoring/main.tf +++ b/blueprints/cloud-operations/quota-monitoring/main.tf @@ -47,7 +47,7 @@ module "pubsub" { } module "cf" { - source = "../../../modules/cloud-function" + source = "../../../modules/cloud-function-v1" project_id = module.project.project_id name = var.name bucket_name = "${var.name}-${random_pet.random.id}" @@ -66,10 +66,8 @@ module "cf" { } service_account_create = true trigger_config = { - v1 = { - event = "google.pubsub.topic.publish" - resource = module.pubsub.topic.id - } + event = "google.pubsub.topic.publish" + resource = module.pubsub.topic.id } } diff --git a/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/main.tf b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/main.tf index 85326edafe..c10c0b6b0f 100644 --- a/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/main.tf +++ b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/main.tf @@ -85,7 +85,7 @@ module "pubsub_file" { ############################################################################### module "cf" { - source = "../../../modules/cloud-function" + source = "../../../modules/cloud-function-v1" project_id = module.project.project_id region = var.region name = var.name @@ -99,16 +99,14 @@ module "cf" { } service_account = module.service-account.email trigger_config = { - v1 = { - event = "google.pubsub.topic.publish" - resource = module.pubsub.topic.id - } + event = "google.pubsub.topic.publish" + resource = module.pubsub.topic.id } } module "cffile" { count = var.cai_gcs_export ? 1 : 0 - source = "../../../modules/cloud-function" + source = "../../../modules/cloud-function-v1" project_id = module.project.project_id region = var.region name = var.name_cffile @@ -124,11 +122,9 @@ module "cffile" { } service_account = module.service-account.email trigger_config = { - v1 = { - event = "google.pubsub.topic.publish" - resource = module.pubsub_file.topic.id - retry = null - } + event = "google.pubsub.topic.publish" + resource = module.pubsub_file.topic.id + retry = null } } diff --git a/blueprints/cloud-operations/unmanaged-instances-healthcheck/main.tf b/blueprints/cloud-operations/unmanaged-instances-healthcheck/main.tf index b1cfb3a291..11e63ee5fd 100644 --- a/blueprints/cloud-operations/unmanaged-instances-healthcheck/main.tf +++ b/blueprints/cloud-operations/unmanaged-instances-healthcheck/main.tf @@ -108,7 +108,7 @@ module "pubsub" { ############################################################################### module "cf-restarter" { - source = "../../../modules/cloud-function" + source = "../../../modules/cloud-function-v1" project_id = module.project.project_id name = "cf-restarter" region = var.region @@ -132,16 +132,14 @@ module "cf-restarter" { } trigger_config = { - v1 = { - event = "google.pubsub.topic.publish" - resource = module.pubsub.topic.id - } + event = "google.pubsub.topic.publish" + resource = module.pubsub.topic.id } } module "cf-healthchecker" { - source = "../../../modules/cloud-function" + source = "../../../modules/cloud-function-v1" project_id = module.project.project_id name = "cf-healthchecker" region = var.region @@ -172,18 +170,14 @@ module "cf-healthchecker" { create = true name = "hc-connector" egress_settings = "PRIVATE_RANGES_ONLY" - } - vpc_connector_config = { ip_cidr_range = "10.132.0.0/28" network = "vpc" } - iam = { "roles/cloudfunctions.invoker" = [module.service-account-scheduler.iam_email] } - depends_on = [ module.vpc ] diff --git a/blueprints/networking/private-cloud-function-from-onprem/main.tf b/blueprints/networking/private-cloud-function-from-onprem/main.tf index b3737bef1a..77b7dfb692 100644 --- a/blueprints/networking/private-cloud-function-from-onprem/main.tf +++ b/blueprints/networking/private-cloud-function-from-onprem/main.tf @@ -177,7 +177,7 @@ module "test-vm" { ############################################################################### module "function-hello" { - source = "../../../modules/cloud-function" + source = "../../../modules/cloud-function-v1" project_id = module.project.project_id name = var.name bucket_name = "${var.name}-tf-cf-deploy" diff --git a/blueprints/serverless/api-gateway/main.tf b/blueprints/serverless/api-gateway/main.tf index fc1b4aa162..d828d5ea44 100644 --- a/blueprints/serverless/api-gateway/main.tf +++ b/blueprints/serverless/api-gateway/main.tf @@ -62,8 +62,8 @@ module "sa" { module "functions" { + source = "../../../modules/cloud-function-v1" for_each = toset(var.regions) - source = "../../../modules/cloud-function" project_id = module.project.project_id name = "${local.function_name_prefix}-${each.value}" bucket_name = "bkt-${module.project.project_id}-${each.value}" diff --git a/modules/README.md b/modules/README.md index eae5aa5990..c1893e376d 100644 --- a/modules/README.md +++ b/modules/README.md @@ -100,5 +100,6 @@ These modules are used in the examples included in this repository. If you are u ## Serverless -- [Cloud Functions](./cloud-function) +- [Cloud Functions v1](./cloud-function-v1) +- [Cloud Functions v2](./cloud-function-v2) - [Cloud Run](./cloud-run) diff --git a/modules/cloud-function-v1/README.md b/modules/cloud-function-v1/README.md new file mode 100644 index 0000000000..5fc72ca3b5 --- /dev/null +++ b/modules/cloud-function-v1/README.md @@ -0,0 +1,236 @@ +# Cloud Function Module (V1) + +Cloud Function management, with support for IAM roles and optional bucket creation. + +The GCS object used for deployment uses a hash of the bundle zip contents in its name, which ensures change tracking and avoids recreating the function if the GCS object is deleted and needs recreating. + +## TODO + +- [ ] add support for `source_repository` + +## Examples + +### 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. + +```hcl +module "cf-http" { + source = "./fabric/modules/cloud-function-v1" + project_id = "my-project" + name = "test-cf-http" + bucket_name = "test-cf-bundles" + bundle_config = { + source_dir = "fabric/assets/" + output_path = "bundle.zip" + } +} +# tftest modules=1 resources=2 +``` + +### PubSub and non-HTTP triggers + +Other trigger types other than HTTP are configured via the `trigger_config` variable. This example shows a PubSub trigger. + +```hcl +module "cf-http" { + source = "./fabric/modules/cloud-function-v1" + project_id = "my-project" + name = "test-cf-http" + bucket_name = "test-cf-bundles" + bundle_config = { + source_dir = "fabric/assets/" + output_path = "bundle.zip" + } + trigger_config = { + event = "google.pubsub.topic.publish" + resource = "local.my-topic" + } +} +# tftest modules=1 resources=2 +``` + +### Controlling HTTP access + +To allow anonymous access to the function, grant the `roles/cloudfunctions.invoker` role to the special `allUsers` identifier. Use specific identities (service accounts, groups, etc.) instead of `allUsers` to only allow selective access. + +```hcl +module "cf-http" { + source = "./fabric/modules/cloud-function-v1" + project_id = "my-project" + name = "test-cf-http" + bucket_name = "test-cf-bundles" + bundle_config = { + source_dir = "fabric/assets/" + output_path = "bundle.zip" + } + iam = { + "roles/cloudfunctions.invoker" = ["allUsers"] + } +} +# tftest modules=1 resources=3 inventory=iam.yaml +``` + +### GCS bucket creation + +You can have the module auto-create the GCS bucket used for deployment via the `bucket_config` variable. Setting `bucket_config.location` to `null` will also use the function region for GCS. + +```hcl +module "cf-http" { + source = "./fabric/modules/cloud-function-v1" + project_id = "my-project" + name = "test-cf-http" + bucket_name = "test-cf-bundles" + bucket_config = { + lifecycle_delete_age_days = 1 + } + bundle_config = { + source_dir = "fabric/assets/" + } +} +# tftest modules=1 resources=3 +``` + +### 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). + +```hcl +module "cf-http" { + source = "./fabric/modules/cloud-function-v1" + project_id = "my-project" + name = "test-cf-http" + bucket_name = "test-cf-bundles" + bundle_config = { + source_dir = "fabric/assets/" + output_path = "bundle.zip" + } + service_account_create = true +} +# tftest modules=1 resources=3 +``` + +To use an externally managed service account, pass its email in `service_account` and leave `service_account_create` to `false` (the default). + +```hcl +module "cf-http" { + source = "./fabric/modules/cloud-function-v1" + project_id = "my-project" + name = "test-cf-http" + bucket_name = "test-cf-bundles" + bundle_config = { + source_dir = "fabric/assets/" + output_path = "bundle.zip" + } + service_account = "non-existent@serice.account.email" +} +# tftest modules=1 resources=2 +``` + +### Custom bundle config + +In order to help prevent `archive_zip.output_md5` from changing cross platform (e.g. Cloud Build vs your local development environment), you'll have to make sure that the files included in the zip are always the same. + +```hcl +module "cf-http" { + source = "./fabric/modules/cloud-function-v1" + project_id = "my-project" + name = "test-cf-http" + bucket_name = "test-cf-bundles" + bundle_config = { + source_dir = "fabric/assets" + output_path = "bundle.zip" + excludes = ["__pycache__"] + } +} +# tftest modules=1 resources=2 +``` + +### Private Cloud Build Pool + +This deploys a Cloud Function with an HTTP endpoint, using a pre-existing GCS bucket for deployment using a pre existing private Cloud Build worker pool. + +```hcl +module "cf-http" { + source = "./fabric/modules/cloud-function-v1" + project_id = "my-project" + name = "test-cf-http" + bucket_name = "test-cf-bundles" + build_worker_pool = "projects/my-project/locations/europe-west1/workerPools/my_build_worker_pool" + bundle_config = { + source_dir = "fabric/assets" + output_path = "bundle.zip" + } +} +# tftest modules=1 resources=2 +``` + +### Multiple Cloud Functions within project + +When deploying multiple functions do not reuse `bundle_config.output_path` between instances as the result is undefined. Default `output_path` creates file in `/tmp` folder using project Id and function name to avoid name conflicts. + +```hcl +module "cf-http-one" { + source = "./fabric/modules/cloud-function-v1" + project_id = "my-project" + name = "test-cf-http-one" + bucket_name = "test-cf-bundles" + bundle_config = { + source_dir = "fabric/assets" + } +} + +module "cf-http-two" { + source = "./fabric/modules/cloud-function-v1" + project_id = "my-project" + name = "test-cf-http-two" + bucket_name = "test-cf-bundles" + bundle_config = { + source_dir = "fabric/assets" + } +} +# tftest modules=2 resources=4 inventory=multiple_functions.yaml +``` + + + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [bucket_name](variables.tf#L26) | 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#L37) | Cloud function source folder and generated zip bundle paths. Output path defaults to '/tmp/bundle.zip' if null. | object({…}) | ✓ | | +| [name](variables.tf#L96) | Name used for cloud function and associated resources. | string | ✓ | | +| [project_id](variables.tf#L111) | Project id 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_worker_pool](variables.tf#L31) | Build worker pool, in projects//locations//workerPools/ format. | string | | null | +| [description](variables.tf#L46) | Optional description. | string | | "Terraform managed." | +| [environment_variables](variables.tf#L52) | Cloud function environment variables. | map(string) | | {} | +| [function_config](variables.tf#L58) | Cloud function configuration. Defaults to using main as entrypoint, 1 instance with 256MiB of memory, and 180 second timeout. | object({…}) | | {…} | +| [iam](variables.tf#L78) | IAM bindings for topic in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [ingress_settings](variables.tf#L84) | Control traffic that reaches the cloud function. Allowed values are ALLOW_ALL, ALLOW_INTERNAL_AND_GCLB and ALLOW_INTERNAL_ONLY . | string | | null | +| [labels](variables.tf#L90) | Resource labels. | map(string) | | {} | +| [prefix](variables.tf#L101) | Optional prefix used for resource names. | string | | null | +| [region](variables.tf#L116) | Region used for all resources. | string | | "europe-west1" | +| [secrets](variables.tf#L122) | Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format. | map(object({…})) | | {} | +| [service_account](variables.tf#L134) | Service account email. Unused if service account is auto-created. | string | | null | +| [service_account_create](variables.tf#L140) | Auto-create service account. | bool | | false | +| [trigger_config](variables.tf#L146) | Function trigger configuration. Leave null for HTTP trigger. | object({…}) | | null | +| [vpc_connector](variables.tf#L156) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | null | +| [vpc_connector_config](variables.tf#L166) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [bucket](outputs.tf#L17) | Bucket resource (only if auto-created). | | +| [bucket_name](outputs.tf#L24) | Bucket name. | | +| [function](outputs.tf#L29) | Cloud function resources. | | +| [function_name](outputs.tf#L34) | Cloud function name. | | +| [id](outputs.tf#L39) | Fully qualified function id. | | +| [service_account](outputs.tf#L44) | Service account resource. | | +| [service_account_email](outputs.tf#L49) | Service account email. | | +| [service_account_iam_email](outputs.tf#L54) | Service account email. | | +| [vpc_connector](outputs.tf#L62) | VPC connector resource if created. | | + + diff --git a/modules/cloud-function-v1/main.tf b/modules/cloud-function-v1/main.tf new file mode 100644 index 0000000000..e965b8ae9e --- /dev/null +++ b/modules/cloud-function-v1/main.tf @@ -0,0 +1,181 @@ +/** + * Copyright 2022 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 { + bucket = ( + var.bucket_name != null + ? var.bucket_name + : ( + length(google_storage_bucket.bucket) > 0 + ? google_storage_bucket.bucket[0].name + : null + ) + ) + prefix = var.prefix == null ? "" : "${var.prefix}-" + service_account_email = ( + var.service_account_create + ? google_service_account.service_account[0].email + : var.service_account + ) + vpc_connector = ( + var.vpc_connector == null + ? null + : ( + try(var.vpc_connector.create, false) == false + ? var.vpc_connector.name + : google_vpc_access_connector.connector.0.id + ) + ) +} + +resource "google_vpc_access_connector" "connector" { + count = try(var.vpc_connector.create, false) == false ? 0 : 1 + 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 +} + +resource "google_cloudfunctions_function" "function" { + project = var.project_id + region = var.region + name = "${local.prefix}${var.name}" + description = var.description + runtime = var.function_config.runtime + available_memory_mb = var.function_config.memory_mb + max_instances = var.function_config.instance_count + timeout = var.function_config.timeout_seconds + entry_point = var.function_config.entry_point + environment_variables = var.environment_variables + service_account_email = local.service_account_email + source_archive_bucket = local.bucket + source_archive_object = google_storage_bucket_object.bundle.name + labels = var.labels + trigger_http = var.trigger_config == null ? true : null + + ingress_settings = var.ingress_settings + build_worker_pool = var.build_worker_pool + + vpc_connector = local.vpc_connector + vpc_connector_egress_settings = try( + var.vpc_connector.egress_settings, null + ) + + dynamic "event_trigger" { + for_each = var.trigger_config == null ? [] : [""] + content { + event_type = var.trigger_config.event + resource = var.trigger_config.resource + dynamic "failure_policy" { + for_each = var.trigger_config.retry == null ? [] : [""] + content { + retry = var.trigger_config.retry + } + } + } + } + + dynamic "secret_environment_variables" { + for_each = { for k, v in var.secrets : k => v if !v.is_volume } + iterator = secret + content { + key = secret.key + project_id = secret.value.project_id + secret = secret.value.secret + version = try(secret.value.versions.0, "latest") + } + } + + dynamic "secret_volumes" { + for_each = { for k, v in var.secrets : k => v if v.is_volume } + iterator = secret + content { + mount_path = secret.key + project_id = secret.value.project_id + secret = secret.value.secret + dynamic "versions" { + for_each = secret.value.versions + iterator = version + content { + path = split(":", version)[1] + version = split(":", version)[0] + } + } + } + } +} + +resource "google_cloudfunctions_function_iam_binding" "default" { + for_each = var.iam + project = var.project_id + region = var.region + cloud_function = google_cloudfunctions_function.function.id + role = each.key + members = each.value +} + +resource "google_storage_bucket" "bucket" { + count = var.bucket_config == null ? 0 : 1 + project = var.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 + ) + labels = var.labels + + dynamic "lifecycle_rule" { + for_each = var.bucket_config.lifecycle_delete_age_days == null ? [] : [""] + content { + action { type = "Delete" } + condition { + age = var.bucket_config.lifecycle_delete_age_days + with_state = "ARCHIVED" + } + } + } + + dynamic "versioning" { + for_each = var.bucket_config.lifecycle_delete_age_days == null ? [] : [""] + content { + enabled = true + } + } +} + +resource "google_storage_bucket_object" "bundle" { + name = "bundle-${data.archive_file.bundle.output_md5}.zip" + bucket = local.bucket + source = data.archive_file.bundle.output_path +} + +data "archive_file" "bundle" { + type = "zip" + source_dir = var.bundle_config.source_dir + output_path = coalesce(var.bundle_config.output_path, "/tmp/bundle-${var.project_id}-${var.name}.zip") + output_file_mode = "0644" + excludes = var.bundle_config.excludes +} + +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}." +} diff --git a/modules/cloud-function-v1/outputs.tf b/modules/cloud-function-v1/outputs.tf new file mode 100644 index 0000000000..9f2fd68ad8 --- /dev/null +++ b/modules/cloud-function-v1/outputs.tf @@ -0,0 +1,65 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +output "bucket" { + description = "Bucket resource (only if auto-created)." + value = try( + var.bucket_config == null ? null : google_storage_bucket.bucket.0, null + ) +} + +output "bucket_name" { + description = "Bucket name." + value = local.bucket +} + +output "function" { + description = "Cloud function resources." + value = google_cloudfunctions_function.function +} + +output "function_name" { + description = "Cloud function name." + value = google_cloudfunctions_function.function.name +} + +output "id" { + description = "Fully qualified function id." + value = google_cloudfunctions_function.function.id +} + +output "service_account" { + description = "Service account resource." + value = try(google_service_account.service_account[0], null) +} + +output "service_account_email" { + description = "Service account email." + value = local.service_account_email +} + +output "service_account_iam_email" { + description = "Service account email." + value = join("", [ + "serviceAccount:", + local.service_account_email == null ? "" : local.service_account_email + ]) +} + +output "vpc_connector" { + description = "VPC connector resource if created." + value = try(google_vpc_access_connector.connector.0.id, null) +} diff --git a/modules/cloud-function/variables.tf b/modules/cloud-function-v1/variables.tf similarity index 84% rename from modules/cloud-function/variables.tf rename to modules/cloud-function-v1/variables.tf index 0bb258ee49..543bc01286 100644 --- a/modules/cloud-function/variables.tf +++ b/modules/cloud-function-v1/variables.tf @@ -146,36 +146,11 @@ variable "service_account_create" { variable "trigger_config" { description = "Function trigger configuration. Leave null for HTTP trigger." type = object({ - v1 = optional(object({ - event = string - resource = string - retry = optional(bool) - })), - v2 = optional(object({ - region = optional(string) - event_type = optional(string) - pubsub_topic = optional(string) - event_filters = optional(list(object({ - attribute = string - value = string - operator = string - }))) - service_account_email = optional(string) - service_account_create = optional(bool) - retry_policy = optional(string) - })) + event = string + resource = string + retry = optional(bool) }) - default = { v1 = null, v2 = null } - validation { - condition = !(var.trigger_config.v1 != null && var.trigger_config.v2 != null) - error_message = "Provide configuration for only one generation - either v1 or v2" - } -} - -variable "v2" { - description = "Whether to use Cloud Function version 2nd Gen or 1st Gen." - type = bool - default = false + default = null } variable "vpc_connector" { diff --git a/modules/cloud-function/versions.tf b/modules/cloud-function-v1/versions.tf similarity index 100% rename from modules/cloud-function/versions.tf rename to modules/cloud-function-v1/versions.tf diff --git a/modules/cloud-function/README.md b/modules/cloud-function-v2/README.md similarity index 77% rename from modules/cloud-function/README.md rename to modules/cloud-function-v2/README.md index 71dd5ef468..e22e656640 100644 --- a/modules/cloud-function/README.md +++ b/modules/cloud-function-v2/README.md @@ -1,4 +1,4 @@ -# Cloud Function Module +# Cloud Function Module (v2) Cloud Function management, with support for IAM roles and optional bucket creation. @@ -16,23 +16,7 @@ This deploys a Cloud Function with an HTTP endpoint, using a pre-existing GCS bu ```hcl module "cf-http" { - source = "./fabric/modules/cloud-function" - project_id = "my-project" - name = "test-cf-http" - bucket_name = "test-cf-bundles" - bundle_config = { - source_dir = "fabric/assets/" - output_path = "bundle.zip" - } -} -# tftest modules=1 resources=2 -``` - -Analogous example using 2nd generation Cloud Functions -```hcl -module "cf-http" { - source = "./fabric/modules/cloud-function" - v2 = true + source = "./fabric/modules/cloud-function-v2" project_id = "my-project" name = "test-cf-http" bucket_name = "test-cf-bundles" @@ -46,30 +30,8 @@ module "cf-http" { ### PubSub and non-HTTP triggers -Other trigger types other than HTTP are configured via the `trigger_config` variable. This example shows a PubSub trigger. - -```hcl -module "cf-http" { - source = "./fabric/modules/cloud-function" - project_id = "my-project" - name = "test-cf-http" - bucket_name = "test-cf-bundles" - bundle_config = { - source_dir = "fabric/assets/" - output_path = "bundle.zip" - } - trigger_config = { - v1 = { - event = "google.pubsub.topic.publish" - resource = "local.my-topic" - } - } -} -# tftest modules=1 resources=2 -``` +Other trigger types other than HTTP are configured via the `trigger_config` variable. This example shows a PubSub trigger via [Eventarc](https://cloud.google.com/eventarc/docs): -Cloud Functions 2nd gen support only [Eventarc](https://cloud.google.com/eventarc/docs) and uses separate structure -to configure: ```hcl module "trigger-service-account" { source = "./fabric/modules/iam-service-account" @@ -83,9 +45,8 @@ module "trigger-service-account" { } module "cf-http" { - source = "./fabric/modules/cloud-function" + source = "./fabric/modules/cloud-function-v2" project_id = "my-project" - v2 = true name = "test-cf-http" bucket_name = "test-cf-bundles" bundle_config = { @@ -93,17 +54,16 @@ module "cf-http" { output_path = "bundle.zip" } trigger_config = { - v2 = { - event_type = "google.cloud.pubsub.topic.v1.messagePublished" - pubsub_topic = "local.my-topic" - service_account_email = module.trigger-service-account.email - } + event_type = "google.cloud.pubsub.topic.v1.messagePublished" + pubsub_topic = "local.my-topic" + service_account_email = module.trigger-service-account.email } } # tftest modules=2 resources=4 ``` -Ensure that pubsub robo-account `service-%s@gcp-sa-pubsub.iam.gserviceaccount.com` has `roles/iam.serviceAccountTokenCreatator` -as documented [here](https://cloud.google.com/eventarc/docs/roles-permissions#pubsub-topic) + +Ensure that pubsub service identity (`service-[project number]@gcp-sa-pubsub.iam.gserviceaccount.com` has `roles/iam.serviceAccountTokenCreator` +as documented [here](https://cloud.google.com/eventarc/docs/roles-permissions#pubsub-topic). ### Controlling HTTP access @@ -111,7 +71,7 @@ To allow anonymous access to the function, grant the `roles/cloudfunctions.invok ```hcl module "cf-http" { - source = "./fabric/modules/cloud-function" + source = "./fabric/modules/cloud-function-v2" project_id = "my-project" name = "test-cf-http" bucket_name = "test-cf-bundles" @@ -123,7 +83,7 @@ module "cf-http" { "roles/cloudfunctions.invoker" = ["allUsers"] } } -# tftest modules=1 resources=3 +# tftest modules=1 resources=3 inventory=iam.yaml ``` ### GCS bucket creation @@ -132,7 +92,7 @@ You can have the module auto-create the GCS bucket used for deployment via the ` ```hcl module "cf-http" { - source = "./fabric/modules/cloud-function" + source = "./fabric/modules/cloud-function-v2" project_id = "my-project" name = "test-cf-http" bucket_name = "test-cf-bundles" @@ -152,7 +112,7 @@ To use a custom service account managed by the module, set `service_account_crea ```hcl module "cf-http" { - source = "./fabric/modules/cloud-function" + source = "./fabric/modules/cloud-function-v2" project_id = "my-project" name = "test-cf-http" bucket_name = "test-cf-bundles" @@ -169,7 +129,7 @@ To use an externally managed service account, pass its email in `service_account ```hcl module "cf-http" { - source = "./fabric/modules/cloud-function" + source = "./fabric/modules/cloud-function-v2" project_id = "my-project" name = "test-cf-http" bucket_name = "test-cf-bundles" @@ -188,7 +148,7 @@ In order to help prevent `archive_zip.output_md5` from changing cross platform ( ```hcl module "cf-http" { - source = "./fabric/modules/cloud-function" + source = "./fabric/modules/cloud-function-v2" project_id = "my-project" name = "test-cf-http" bucket_name = "test-cf-bundles" @@ -203,11 +163,11 @@ module "cf-http" { ### Private Cloud Build Pool -This deploys a Cloud Function with an HTTP endpoint, using a pre-existing GCS bucket for deployment using a pre existing private Cloud Build worker pool. +This deploys a Cloud Function with an HTTP endpoint, using a pre-existing GCS bucket for deployment using a pre existing private Cloud Build worker pool. ```hcl module "cf-http" { - source = "./fabric/modules/cloud-function" + source = "./fabric/modules/cloud-function-v2" project_id = "my-project" name = "test-cf-http" bucket_name = "test-cf-bundles" @@ -226,7 +186,7 @@ When deploying multiple functions do not reuse `bundle_config.output_path` betwe ```hcl module "cf-http-one" { - source = "./fabric/modules/cloud-function" + source = "./fabric/modules/cloud-function-v2" project_id = "my-project" name = "test-cf-http-one" bucket_name = "test-cf-bundles" @@ -236,7 +196,7 @@ module "cf-http-one" { } module "cf-http-two" { - source = "./fabric/modules/cloud-function" + source = "./fabric/modules/cloud-function-v2" project_id = "my-project" name = "test-cf-http-two" bucket_name = "test-cf-bundles" @@ -245,8 +205,6 @@ module "cf-http-two" { } } # tftest modules=2 resources=4 inventory=multiple_functions.yaml - - ``` @@ -271,10 +229,9 @@ module "cf-http-two" { | [secrets](variables.tf#L122) | Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format. | map(object({…})) | | {} | | [service_account](variables.tf#L134) | Service account email. Unused if service account is auto-created. | string | | null | | [service_account_create](variables.tf#L140) | Auto-create service account. | bool | | false | -| [trigger_config](variables.tf#L146) | Function trigger configuration. Leave null for HTTP trigger. | object({…}) | | { v1 = null, v2 = null } | -| [v2](variables.tf#L175) | Whether to use Cloud Function version 2nd Gen or 1st Gen. | bool | | false | -| [vpc_connector](variables.tf#L181) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | null | -| [vpc_connector_config](variables.tf#L191) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | +| [trigger_config](variables.tf#L146) | Function trigger configuration. Leave null for HTTP trigger. | object({…}) | | null | +| [vpc_connector](variables.tf#L164) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | null | +| [vpc_connector_config](variables.tf#L174) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | ## Outputs diff --git a/modules/cloud-function/main.tf b/modules/cloud-function-v2/main.tf similarity index 59% rename from modules/cloud-function/main.tf rename to modules/cloud-function-v2/main.tf index 2078a48334..4e8cca366f 100644 --- a/modules/cloud-function/main.tf +++ b/modules/cloud-function-v2/main.tf @@ -24,15 +24,14 @@ locals { : null ) ) - function = ( - var.v2 - ? google_cloudfunctions2_function.function[0] - : google_cloudfunctions_function.function[0] + prefix = var.prefix == null ? "" : "${var.prefix}-" + service_account_email = ( + var.service_account_create + ? google_service_account.service_account[0].email + : var.service_account ) - prefix = var.prefix == null ? "" : "${var.prefix}-" - service_account_email = var.service_account_create ? google_service_account.service_account[0].email : var.service_account trigger_service_account_email = ( - coalesce(try(var.trigger_config.v2.service_account_create, false), false) + try(var.trigger_config.service_account_create, false) ? google_service_account.trigger_service_account[0].email : null ) @@ -48,7 +47,7 @@ locals { } resource "google_vpc_access_connector" "connector" { - count = try(var.vpc_connector.create, false) == false ? 0 : 1 + count = try(var.vpc_connector.create, false) == true ? 1 : 0 project = var.project_id name = var.vpc_connector.name region = var.region @@ -56,78 +55,7 @@ resource "google_vpc_access_connector" "connector" { network = var.vpc_connector_config.network } -resource "google_cloudfunctions_function" "function" { - count = var.v2 ? 0 : 1 - project = var.project_id - region = var.region - name = "${local.prefix}${var.name}" - description = var.description - runtime = var.function_config.runtime - available_memory_mb = var.function_config.memory_mb - max_instances = var.function_config.instance_count - timeout = var.function_config.timeout_seconds - entry_point = var.function_config.entry_point - environment_variables = var.environment_variables - service_account_email = local.service_account_email - source_archive_bucket = local.bucket - source_archive_object = google_storage_bucket_object.bundle.name - labels = var.labels - trigger_http = var.trigger_config.v1 == null ? true : null - - ingress_settings = var.ingress_settings - build_worker_pool = var.build_worker_pool - - vpc_connector = local.vpc_connector - vpc_connector_egress_settings = try( - var.vpc_connector.egress_settings, null - ) - - dynamic "event_trigger" { - for_each = var.trigger_config.v1 == null ? [] : [""] - content { - event_type = var.trigger_config.v1.event - resource = var.trigger_config.v1.resource - dynamic "failure_policy" { - for_each = var.trigger_config.v1.retry == null ? [] : [""] - content { - retry = var.trigger_config.v1.retry - } - } - } - } - - dynamic "secret_environment_variables" { - for_each = { for k, v in var.secrets : k => v if !v.is_volume } - iterator = secret - content { - key = secret.key - project_id = secret.value.project_id - secret = secret.value.secret - version = try(secret.value.versions.0, "latest") - } - } - - dynamic "secret_volumes" { - for_each = { for k, v in var.secrets : k => v if v.is_volume } - iterator = secret - content { - mount_path = secret.key - project_id = secret.value.project_id - secret = secret.value.secret - dynamic "versions" { - for_each = secret.value.versions - iterator = version - content { - path = split(":", version)[1] - version = split(":", version)[0] - } - } - } - } -} - resource "google_cloudfunctions2_function" "function" { - count = var.v2 ? 1 : 0 provider = google-beta project = var.project_id location = var.region @@ -136,7 +64,7 @@ resource "google_cloudfunctions2_function" "function" { build_config { worker_pool = var.build_worker_pool runtime = var.function_config.runtime - entry_point = "${var.function_config.entry_point}_http" # Set the entry point + entry_point = var.function_config.entry_point environment_variables = var.environment_variables source { storage_source { @@ -146,13 +74,17 @@ resource "google_cloudfunctions2_function" "function" { } } dynamic "event_trigger" { - for_each = var.trigger_config.v2 == null ? [] : [""] + for_each = var.trigger_config == null ? [] : [""] content { - trigger_region = var.trigger_config.v2.region - event_type = var.trigger_config.v2.event_type - pubsub_topic = var.trigger_config.v2.pubsub_topic + event_type = var.trigger_config.event_type + pubsub_topic = var.trigger_config.pubsub_topic + trigger_region = ( + var.trigger_config.region == null + ? var.region + : var.trigger_config.region + ) dynamic "event_filters" { - for_each = var.trigger_config.v2.event_filters == null ? [] : var.trigger_config.v2.event_filters + for_each = var.trigger_config.event_filters iterator = event_filter content { attribute = event_filter.attribute @@ -160,8 +92,8 @@ resource "google_cloudfunctions2_function" "function" { operator = event_filter.operator } } - service_account_email = var.trigger_config.v2.service_account_email - retry_policy = var.trigger_config.v2.retry_policy + service_account_email = var.trigger_config.service_account_email + retry_policy = var.trigger_config.retry_policy } } service_config { @@ -210,20 +142,11 @@ resource "google_cloudfunctions2_function" "function" { labels = var.labels } -resource "google_cloudfunctions_function_iam_binding" "default" { - for_each = !var.v2 ? var.iam : {} - project = var.project_id - region = var.region - cloud_function = local.function.name - role = each.key - members = each.value -} - resource "google_cloudfunctions2_function_iam_binding" "default" { - for_each = var.v2 ? var.iam : {} + for_each = var.iam project = var.project_id - location = google_cloudfunctions2_function.function[0].location - cloud_function = local.function.name + location = google_cloudfunctions2_function.function.location + cloud_function = google_cloudfunctions2_function.function.name role = each.key members = each.value } @@ -281,14 +204,18 @@ resource "google_service_account" "service_account" { } resource "google_service_account" "trigger_service_account" { - count = coalesce(try(var.trigger_config.v2.service_account_create, false), false) ? 1 : 0 + count = ( + try(var.trigger_config.service_account_create, false) == true ? 1 : 0 + ) project = var.project_id account_id = "tf-cf-trigger-${var.name}" display_name = "Terraform trigger for Cloud Function ${var.name}." } resource "google_project_iam_member" "trigger_iam" { - count = coalesce(try(var.trigger_config.v2.service_account_create, false), false) ? 1 : 0 + count = ( + try(var.trigger_config.service_account_create, false) == true ? 1 : 0 + ) project = var.project_id member = "serviceAccount:${google_service_account.trigger_service_account[0].email}" role = "roles/run.invoker" diff --git a/modules/cloud-function/outputs.tf b/modules/cloud-function-v2/outputs.tf similarity index 89% rename from modules/cloud-function/outputs.tf rename to modules/cloud-function-v2/outputs.tf index 1f18798a66..780f5c1e49 100644 --- a/modules/cloud-function/outputs.tf +++ b/modules/cloud-function-v2/outputs.tf @@ -28,17 +28,17 @@ output "bucket_name" { output "function" { description = "Cloud function resources." - value = local.function + value = google_cloudfunctions2_function.function } output "function_name" { description = "Cloud function name." - value = local.function.name + value = google_cloudfunctions2_function.function.name } output "id" { description = "Fully qualified function id." - value = local.function.id + value = google_cloudfunctions2_function.function.id } output "service_account" { @@ -79,7 +79,7 @@ output "trigger_service_account_iam_email" { output "uri" { description = "Cloud function service uri." - value = var.v2 ? google_cloudfunctions2_function.function[0].service_config[0].uri : null + value = google_cloudfunctions2_function.function.service_config[0].uri } output "vpc_connector" { diff --git a/modules/cloud-function-v2/variables.tf b/modules/cloud-function-v2/variables.tf new file mode 100644 index 0000000000..53437ef1ea --- /dev/null +++ b/modules/cloud-function-v2/variables.tf @@ -0,0 +1,183 @@ +/** + * Copyright 2022 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 "bucket_config" { + description = "Enable and configure auto-created bucket. Set fields to null to use defaults." + type = object({ + location = optional(string) + lifecycle_delete_age_days = optional(number) + }) + default = null +} + +variable "bucket_name" { + description = "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." + type = string +} + +variable "build_worker_pool" { + description = "Build worker pool, in projects//locations//workerPools/ format." + type = string + default = null +} + +variable "bundle_config" { + description = "Cloud function source folder and generated zip bundle paths. Output path defaults to '/tmp/bundle.zip' if null." + type = object({ + source_dir = string + output_path = optional(string) + excludes = optional(list(string)) + }) +} + +variable "description" { + description = "Optional description." + type = string + default = "Terraform managed." +} + +variable "environment_variables" { + description = "Cloud function environment variables." + type = map(string) + default = {} +} + +variable "function_config" { + description = "Cloud function configuration. Defaults to using main as entrypoint, 1 instance with 256MiB of memory, and 180 second timeout." + type = object({ + entry_point = optional(string, "main") + instance_count = optional(number, 1) + memory_mb = optional(number, 256) # Memory in MB + cpu = optional(string, "0.166") + runtime = optional(string, "python310") + timeout_seconds = optional(number, 180) + }) + default = { + entry_point = "main" + instance_count = 1 + memory_mb = 256 + cpu = "0.166" + runtime = "python310" + timeout_seconds = 180 + } +} + +variable "iam" { + description = "IAM bindings for topic in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} +} + +variable "ingress_settings" { + description = "Control traffic that reaches the cloud function. Allowed values are ALLOW_ALL, ALLOW_INTERNAL_AND_GCLB and ALLOW_INTERNAL_ONLY ." + type = string + default = null +} + +variable "labels" { + description = "Resource labels." + type = map(string) + default = {} +} + +variable "name" { + description = "Name used for cloud function and associated resources." + type = string +} + +variable "prefix" { + description = "Optional prefix used for resource names." + type = string + default = null + validation { + condition = var.prefix != "" + error_message = "Prefix cannot be empty, please use null instead." + } +} + +variable "project_id" { + description = "Project id used for all resources." + type = string +} + +variable "region" { + description = "Region used for all resources." + type = string + default = "europe-west1" +} + +variable "secrets" { + description = "Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format." + type = map(object({ + is_volume = bool + project_id = number + secret = string + versions = list(string) + })) + nullable = false + 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({ + event_type = string + pubsub_topic = optional(string) + region = optional(string) + event_filters = optional(list(object({ + attribute = string + value = string + operator = string + })), []) + service_account_email = optional(string) + service_account_create = optional(bool, false) + retry_policy = optional(string) + }) + default = null +} + +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 + }) + default = null +} + +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 + }) + default = null +} + + diff --git a/tests/modules/cloud_function/__init__.py b/modules/cloud-function-v2/versions.tf similarity index 61% rename from tests/modules/cloud_function/__init__.py rename to modules/cloud-function-v2/versions.tf index 6d6d1266c3..601cb7651f 100644 --- a/tests/modules/cloud_function/__init__.py +++ b/modules/cloud-function-v2/versions.tf @@ -4,10 +4,26 @@ # 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 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +terraform { + required_version = ">= 1.4.4" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.69.0" # tftest + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.69.0" # tftest + } + } +} + + diff --git a/tests/modules/cloud_function/common.tfvars b/tests/modules/cloud_function/common.tfvars deleted file mode 100644 index d7c7350ceb..0000000000 --- a/tests/modules/cloud_function/common.tfvars +++ /dev/null @@ -1,11 +0,0 @@ -project_id = "my-project" -name = "test" -bucket_name = "mybucket" -bundle_config = { - source_dir = "../../tests/modules/cloud_function/bundle" - output_path = "bundle.zip" - excludes = null -} -iam = { - "roles/cloudfunctions.invoker" = ["allUsers"] -} diff --git a/tests/modules/cloud_function/fixture/common.tfvars b/tests/modules/cloud_function/fixture/common.tfvars deleted file mode 100644 index f9fb11f4dd..0000000000 --- a/tests/modules/cloud_function/fixture/common.tfvars +++ /dev/null @@ -1,12 +0,0 @@ -project_id = "my-project" -name = "test" -bucket_name = var.bucket_name -v2 = var.v2 -bundle_config = { - source_dir = "bundle" - output_path = "bundle.zip" - excludes = null -} -iam = { - "roles/cloudfunctions.invoker" = ["allUsers"] -} diff --git a/tests/modules/cloud_function/fixture/main.tf b/tests/modules/cloud_function/fixture/main.tf deleted file mode 100644 index 0096159fbc..0000000000 --- a/tests/modules/cloud_function/fixture/main.tf +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright 2022 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 "test" { - source = "../../../../modules/cloud-function" - project_id = "my-project" - name = "test" - bucket_name = var.bucket_name - v2 = var.v2 - bundle_config = { - source_dir = "bundle" - output_path = "bundle.zip" - excludes = null - } - iam = { - "roles/cloudfunctions.invoker" = ["allUsers"] - } -} diff --git a/tests/modules/cloud_function/fixture/variables.tf b/tests/modules/cloud_function/fixture/variables.tf deleted file mode 100644 index 1923862768..0000000000 --- a/tests/modules/cloud_function/fixture/variables.tf +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright 2022 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 "bucket_name" { - type = any - default = "test" -} - -variable "v2" { - type = any - default = false -} diff --git a/tests/modules/cloud_function/test_plan.py b/tests/modules/cloud_function/test_plan.py deleted file mode 100644 index cd2eab9e41..0000000000 --- a/tests/modules/cloud_function/test_plan.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# 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. - -import pytest - - -@pytest.fixture -def resources(plan_summary, version): - # convert `version` to a boolean suitable for the `v2` variable - v2 = {'v1': 'false', 'v2': 'true'}[version] - summary = plan_summary('modules/cloud-function', - tf_var_files=['common.tfvars'], v2=v2) - return summary - - -@pytest.mark.parametrize('version', ['v1', 'v2']) -def test_resource_count(resources): - "Test number of resources created." - assert resources.counts['resources'] == 3 - - -@pytest.mark.parametrize('version', ['v1', 'v2']) -def test_iam(resources, version): - "Test IAM binding resources." - type = { - 'v1': 'google_cloudfunctions_function_iam_binding', - 'v2': 'google_cloudfunctions2_function_iam_binding' - }[version] - key = f'{type}.default["roles/cloudfunctions.invoker"]' - binding = resources.values[key] - assert binding['role'] == 'roles/cloudfunctions.invoker' - assert binding['members'] == ['allUsers'] diff --git a/tests/modules/cloud_function_v1/examples/iam.yaml b/tests/modules/cloud_function_v1/examples/iam.yaml new file mode 100644 index 0000000000..363f970b6a --- /dev/null +++ b/tests/modules/cloud_function_v1/examples/iam.yaml @@ -0,0 +1,28 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# 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_iam_binding.default["roles/cloudfunctions.invoker"]: + condition: [] + members: + - allUsers + project: my-project + region: europe-west1 + role: roles/cloudfunctions.invoker + +counts: + google_cloudfunctions_function: 1 + google_storage_bucket_object: 1 + modules: 1 + resources: 3 diff --git a/tests/modules/cloud_function/examples/multiple_functions.yaml b/tests/modules/cloud_function_v1/examples/multiple_functions.yaml similarity index 100% rename from tests/modules/cloud_function/examples/multiple_functions.yaml rename to tests/modules/cloud_function_v1/examples/multiple_functions.yaml diff --git a/tests/modules/cloud_function_v2/examples/iam.yaml b/tests/modules/cloud_function_v2/examples/iam.yaml new file mode 100644 index 0000000000..6353b62615 --- /dev/null +++ b/tests/modules/cloud_function_v2/examples/iam.yaml @@ -0,0 +1,29 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# 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_iam_binding.default["roles/cloudfunctions.invoker"]: + cloud_function: test-cf-http + condition: [] + location: europe-west1 + members: + - allUsers + project: my-project + role: roles/cloudfunctions.invoker + +counts: + google_cloudfunctions2_function: 1 + google_storage_bucket_object: 1 + modules: 1 + resources: 3 diff --git a/tests/modules/cloud_function/bundle/main.py b/tests/modules/cloud_function_v2/examples/multiple_functions.yaml similarity index 59% rename from tests/modules/cloud_function/bundle/main.py rename to tests/modules/cloud_function_v2/examples/multiple_functions.yaml index 6d6d1266c3..bcff9c270e 100644 --- a/tests/modules/cloud_function/bundle/main.py +++ b/tests/modules/cloud_function_v2/examples/multiple_functions.yaml @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,3 +11,15 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +values: + module.cf-http-one.google_storage_bucket_object.bundle: + source: /tmp/bundle-my-project-test-cf-http-one.zip + module.cf-http-two.google_storage_bucket_object.bundle: + source: /tmp/bundle-my-project-test-cf-http-two.zip + +counts: + google_cloudfunctions2_function: 2 + google_storage_bucket_object: 2 + modules: 2 + resources: 4 From 4e18def0c608c2e9c70c6cbf1c93cf237f1a8b36 Mon Sep 17 00:00:00 2001 From: Albert Lloveras Date: Tue, 20 Jun 2023 09:53:08 +1000 Subject: [PATCH 2/5] =?UTF-8?q?fixup(project-factory):=20Use=20the=20corre?= =?UTF-8?q?ct=20KMS=20Service=20Agents=20attribute=20=E2=80=A6=20(#1446)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fixup(project-factory): Use the correct KMS Service Agents attribute name * Add new KMS bindings to tests * Update test resource counts * Update README.md resource count --- blueprints/factories/project-factory/README.md | 4 ++-- fast/stages/3-project-factory/dev/main.tf | 2 +- .../project_factory/examples/example.yaml | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/blueprints/factories/project-factory/README.md b/blueprints/factories/project-factory/README.md index d374dceb0a..927edd73c8 100644 --- a/blueprints/factories/project-factory/README.md +++ b/blueprints/factories/project-factory/README.md @@ -67,7 +67,7 @@ module "projects" { folder_id = each.value.folder_id group_iam = try(each.value.group_iam, {}) iam = try(each.value.iam, {}) - kms_service_agents = try(each.value.kms, {}) + kms_service_agents = try(each.value.kms_service_agents, {}) labels = try(each.value.labels, {}) org_policies = try(each.value.org_policies, {}) prefix = each.value.prefix @@ -76,7 +76,7 @@ module "projects" { service_identities_iam = try(each.value.service_identities_iam, {}) vpc = try(each.value.vpc, null) } -# tftest modules=7 resources=30 inventory=example.yaml +# tftest modules=7 resources=34 inventory=example.yaml ``` ### Projects configuration diff --git a/fast/stages/3-project-factory/dev/main.tf b/fast/stages/3-project-factory/dev/main.tf index e0deb24856..e38348fe9a 100644 --- a/fast/stages/3-project-factory/dev/main.tf +++ b/fast/stages/3-project-factory/dev/main.tf @@ -44,7 +44,7 @@ module "projects" { folder_id = try(each.value.folder_id, local.defaults.folder_id) group_iam = try(each.value.group_iam, {}) iam = try(each.value.iam, {}) - kms_service_agents = try(each.value.kms, {}) + kms_service_agents = try(each.value.kms_service_agents, {}) labels = try(each.value.labels, {}) org_policies = try(each.value.org_policies, null) prefix = var.prefix diff --git a/tests/blueprints/factories/project_factory/examples/example.yaml b/tests/blueprints/factories/project_factory/examples/example.yaml index fe33a437d6..f8396ef1d1 100644 --- a/tests/blueprints/factories/project_factory/examples/example.yaml +++ b/tests/blueprints/factories/project_factory/examples/example.yaml @@ -170,6 +170,22 @@ values: condition: [] project: fast-dev-net-spoke-0 role: roles/compute.securityAdmin + module.projects["project"].module.project.google_kms_crypto_key_iam_member.service_identity_cmek["compute.key1"]: + condition: [] + crypto_key_id: key1 + role: roles/cloudkms.cryptoKeyEncrypterDecrypter + module.projects["project"].module.project.google_kms_crypto_key_iam_member.service_identity_cmek["compute.key2"]: + condition: [] + crypto_key_id: key2 + role: roles/cloudkms.cryptoKeyEncrypterDecrypter + module.projects["project"].module.project.google_kms_crypto_key_iam_member.service_identity_cmek["storage.key1"]: + condition: [] + crypto_key_id: key1 + role: roles/cloudkms.cryptoKeyEncrypterDecrypter + module.projects["project"].module.project.google_kms_crypto_key_iam_member.service_identity_cmek["storage.key2"]: + condition: [] + crypto_key_id: key2 + role: roles/cloudkms.cryptoKeyEncrypterDecrypter module.projects["project"].module.project.google_project_service.project_services["billingbudgets.googleapis.com"]: disable_dependent_services: false disable_on_destroy: false @@ -233,3 +249,4 @@ counts: google_project_service: 8 google_service_account: 2 google_storage_project_service_account: 1 + google_kms_crypto_key_iam_member: 4 From c05bc41b691695197fbdfb280f9ac436041394f3 Mon Sep 17 00:00:00 2001 From: Albert Lloveras Date: Tue, 20 Jun 2023 10:17:59 +1000 Subject: [PATCH 3/5] feat(artifact-registry): Add support for CMEK --- modules/artifact-registry/README.md | 3 ++- modules/artifact-registry/main.tf | 1 + modules/artifact-registry/variables.tf | 6 ++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/modules/artifact-registry/README.md b/modules/artifact-registry/README.md index b782c06888..6e3d0340c1 100644 --- a/modules/artifact-registry/README.md +++ b/modules/artifact-registry/README.md @@ -13,7 +13,7 @@ module "docker_artifact_registry" { location = "europe-west1" format = "DOCKER" id = "myregistry" - iam = { + iam = { "roles/artifactregistry.admin" = ["group:cicd@example.com"] } } @@ -28,6 +28,7 @@ module "docker_artifact_registry" { | [id](variables.tf#L35) | Repository id. | string | ✓ | | | [project_id](variables.tf#L52) | Registry project id. | string | ✓ | | | [description](variables.tf#L17) | An optional description for the repository. | string | | "Terraform-managed registry" | +| [encryption_key](variables.tf#L57) | The KMS key name to use for encryption at rest. | string | | null | | [format](variables.tf#L23) | Repository format. One of DOCKER or UNSPECIFIED. | string | | "DOCKER" | | [iam](variables.tf#L29) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [labels](variables.tf#L40) | Labels to be attached to the registry. | map(string) | | {} | diff --git a/modules/artifact-registry/main.tf b/modules/artifact-registry/main.tf index 8b01e09619..814aaba140 100644 --- a/modules/artifact-registry/main.tf +++ b/modules/artifact-registry/main.tf @@ -22,6 +22,7 @@ resource "google_artifact_registry_repository" "registry" { format = var.format labels = var.labels repository_id = var.id + kms_key_name = var.encryption_key } resource "google_artifact_registry_repository_iam_binding" "bindings" { diff --git a/modules/artifact-registry/variables.tf b/modules/artifact-registry/variables.tf index 907ee976b6..8fcd2c4d28 100644 --- a/modules/artifact-registry/variables.tf +++ b/modules/artifact-registry/variables.tf @@ -53,3 +53,9 @@ variable "project_id" { description = "Registry project id." type = string } + +variable "encryption_key" { + description = "The KMS key name to use for encryption at rest." + type = string + default = null +} From 1f6f0c306d3e5b0d26561b0fe94e5bc2bc5cfc8b Mon Sep 17 00:00:00 2001 From: Albert Lloveras Date: Tue, 20 Jun 2023 15:53:37 +1000 Subject: [PATCH 4/5] Formatting --- modules/artifact-registry/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/artifact-registry/README.md b/modules/artifact-registry/README.md index 6e3d0340c1..543de78b2d 100644 --- a/modules/artifact-registry/README.md +++ b/modules/artifact-registry/README.md @@ -13,7 +13,7 @@ module "docker_artifact_registry" { location = "europe-west1" format = "DOCKER" id = "myregistry" - iam = { + iam = { "roles/artifactregistry.admin" = ["group:cicd@example.com"] } } From 97d6e48bde98a22c22c55aba355519d16c45aacf Mon Sep 17 00:00:00 2001 From: Albert Lloveras Date: Tue, 20 Jun 2023 17:27:15 +1000 Subject: [PATCH 5/5] Re-order variables --- modules/artifact-registry/README.md | 14 +++++++------- modules/artifact-registry/variables.tf | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/modules/artifact-registry/README.md b/modules/artifact-registry/README.md index 543de78b2d..26a9c3fb06 100644 --- a/modules/artifact-registry/README.md +++ b/modules/artifact-registry/README.md @@ -25,14 +25,14 @@ module "docker_artifact_registry" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [id](variables.tf#L35) | Repository id. | string | ✓ | | -| [project_id](variables.tf#L52) | Registry project id. | string | ✓ | | +| [id](variables.tf#L41) | Repository id. | string | ✓ | | +| [project_id](variables.tf#L58) | Registry project id. | string | ✓ | | | [description](variables.tf#L17) | An optional description for the repository. | string | | "Terraform-managed registry" | -| [encryption_key](variables.tf#L57) | The KMS key name to use for encryption at rest. | string | | null | -| [format](variables.tf#L23) | Repository format. One of DOCKER or UNSPECIFIED. | string | | "DOCKER" | -| [iam](variables.tf#L29) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [labels](variables.tf#L40) | Labels to be attached to the registry. | map(string) | | {} | -| [location](variables.tf#L46) | Registry location. Use `gcloud beta artifacts locations list' to get valid values. | string | | null | +| [encryption_key](variables.tf#L23) | The KMS key name to use for encryption at rest. | string | | null | +| [format](variables.tf#L29) | Repository format. One of DOCKER or UNSPECIFIED. | string | | "DOCKER" | +| [iam](variables.tf#L35) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [labels](variables.tf#L46) | Labels to be attached to the registry. | map(string) | | {} | +| [location](variables.tf#L52) | Registry location. Use `gcloud beta artifacts locations list' to get valid values. | string | | null | ## Outputs diff --git a/modules/artifact-registry/variables.tf b/modules/artifact-registry/variables.tf index 8fcd2c4d28..afdfa8d513 100644 --- a/modules/artifact-registry/variables.tf +++ b/modules/artifact-registry/variables.tf @@ -20,6 +20,12 @@ variable "description" { default = "Terraform-managed registry" } +variable "encryption_key" { + description = "The KMS key name to use for encryption at rest." + type = string + default = null +} + variable "format" { description = "Repository format. One of DOCKER or UNSPECIFIED." type = string @@ -53,9 +59,3 @@ variable "project_id" { description = "Registry project id." type = string } - -variable "encryption_key" { - description = "The KMS key name to use for encryption at rest." - type = string - default = null -}