Cloud Function v2 - contexts and interface refactor

This commit is contained in:
Wiktor Niesiobędzki
2025-10-20 11:10:01 +00:00
parent e17d2d1dc5
commit 36f2e65465
17 changed files with 868 additions and 90 deletions

View File

@@ -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
```
<!-- BEGIN TFDOC -->
## 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. | <code>string</code> | ✓ | |
| [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. | <code title="object&#40;&#123;&#10; path &#61; string&#10; folder_options &#61; optional&#40;object&#40;&#123;&#10; archive_path &#61; optional&#40;string&#41;&#10; excludes &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [name](variables.tf#L149) | Name used for cloud function and associated resources. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L164) | Project id used for all resources. | <code>string</code> | ✓ | |
| [region](variables.tf#L169) | Region used for all resources. | <code>string</code> | ✓ | |
| [name](variables.tf#L167) | Name used for cloud function and associated resources. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L182) | Project id used for all resources. | <code>string</code> | ✓ | |
| [region](variables.tf#L187) | Region used for all resources. | <code>string</code> | ✓ | |
| [bucket_config](variables.tf#L17) | Enable and configure auto-created bucket. Set fields to null to use defaults. | <code title="object&#40;&#123;&#10; force_destroy &#61; optional&#40;bool&#41;&#10; lifecycle_delete_age_days &#61; optional&#40;number&#41;&#10; location &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [build_environment_variables](variables.tf#L33) | A set of key/value environment variable pairs available during build time. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [build_service_account](variables.tf#L39) | Build service account email. | <code>string</code> | | <code>null</code> |
| [build_worker_pool](variables.tf#L45) | Build worker pool, in projects/<PROJECT-ID>/locations/<REGION>/workerPools/<POOL_NAME> format. | <code>string</code> | | <code>null</code> |
| [description](variables.tf#L84) | Optional description. | <code>string</code> | | <code>&#34;Terraform managed.&#34;</code> |
| [docker_repository_id](variables.tf#L90) | User managed repository created in Artifact Registry. | <code>string</code> | | <code>null</code> |
| [environment_variables](variables.tf#L96) | Cloud function environment variables. | <code>map&#40;string&#41;</code> | | <code title="&#123;&#10; LOG_EXECUTION_ID &#61; &#34;true&#34;&#10;&#125;">&#123;&#8230;&#125;</code> |
| [function_config](variables.tf#L104) | Cloud function configuration. Defaults to using main as entrypoint, 1 instance with 256MiB of memory, and 180 second timeout. | <code title="object&#40;&#123;&#10; binary_authorization_policy &#61; optional&#40;string&#41;&#10; entry_point &#61; optional&#40;string, &#34;main&#34;&#41;&#10; instance_count &#61; optional&#40;number, 1&#41;&#10; memory_mb &#61; optional&#40;number, 256&#41; &#35; Memory in MB&#10; cpu &#61; optional&#40;string, &#34;0.166&#34;&#41;&#10; runtime &#61; optional&#40;string, &#34;python310&#34;&#41;&#10; timeout_seconds &#61; optional&#40;number, 180&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; entry_point &#61; &#34;main&#34;&#10; instance_count &#61; 1&#10; memory_mb &#61; 256&#10; cpu &#61; &#34;0.166&#34;&#10; runtime &#61; &#34;python310&#34;&#10; timeout_seconds &#61; 180&#10;&#125;">&#123;&#8230;&#125;</code> |
| [iam](variables.tf#L125) | IAM bindings for topic in {ROLE => [MEMBERS]} format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [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 . | <code>string</code> | | <code>null</code> |
| [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. | <code>string</code> | | <code>null</code> |
| [labels](variables.tf#L143) | Resource labels. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [prefix](variables.tf#L154) | Optional prefix used for resource names. | <code>string</code> | | <code>null</code> |
| [secrets](variables.tf#L174) | Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format. | <code title="map&#40;object&#40;&#123;&#10; is_volume &#61; bool&#10; project_id &#61; string&#10; secret &#61; string&#10; versions &#61; list&#40;string&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [service_account](variables.tf#L186) | Service account email. Unused if service account is auto-created. | <code>string</code> | | <code>null</code> |
| [service_account_create](variables.tf#L192) | Auto-create service account. | <code>bool</code> | | <code>false</code> |
| [trigger_config](variables.tf#L198) | Function trigger configuration. Leave null for HTTP trigger. | <code title="object&#40;&#123;&#10; event_type &#61; string&#10; pubsub_topic &#61; optional&#40;string&#41;&#10; region &#61; optional&#40;string&#41;&#10; event_filters &#61; optional&#40;list&#40;object&#40;&#123;&#10; attribute &#61; string&#10; value &#61; string&#10; operator &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;, &#91;&#93;&#41;&#10; service_account_email &#61; optional&#40;string&#41;&#10; service_account_create &#61; optional&#40;bool, false&#41;&#10; retry_policy &#61; optional&#40;string, &#34;RETRY_POLICY_DO_NOT_RETRY&#34;&#41; &#35; default to avoid permadiff&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [vpc_connector](variables.tf#L216) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | <code title="object&#40;&#123;&#10; create &#61; optional&#40;bool, false&#41;&#10; name &#61; optional&#40;string&#41;&#10; egress_settings &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [vpc_connector_config](variables.tf#L227) | VPC connector network configuration. Must be provided if new VPC connector is being created. | <code title="object&#40;&#123;&#10; ip_cidr_range &#61; string&#10; network &#61; string&#10; instances &#61; optional&#40;object&#40;&#123;&#10; max &#61; optional&#40;number&#41;&#10; min &#61; optional&#40;number, 2&#41;&#10; &#125;&#41;&#41;&#10; throughput &#61; optional&#40;object&#40;&#123;&#10; max &#61; optional&#40;number, 300&#41;&#10; min &#61; optional&#40;number, 200&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [context](variables.tf#L84) | Context-specific interpolations. | <code title="object&#40;&#123;&#10; condition_vars &#61; optional&#40;map&#40;map&#40;string&#41;&#41;, &#123;&#125;&#41; &#35; not needed here&#63;&#10; cidr_ranges &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; custom_roles &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; iam_principals &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; kms_keys &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; locations &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; networks &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; project_ids &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; subnets &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; tag_values &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41; &#35; not needed here&#63;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [description](variables.tf#L102) | Optional description. | <code>string</code> | | <code>&#34;Terraform managed.&#34;</code> |
| [docker_repository_id](variables.tf#L108) | User managed repository created in Artifact Registry. | <code>string</code> | | <code>null</code> |
| [environment_variables](variables.tf#L114) | Cloud function environment variables. | <code>map&#40;string&#41;</code> | | <code title="&#123;&#10; LOG_EXECUTION_ID &#61; &#34;true&#34;&#10;&#125;">&#123;&#8230;&#125;</code> |
| [function_config](variables.tf#L122) | Cloud function configuration. Defaults to using main as entrypoint, 1 instance with 256MiB of memory, and 180 second timeout. | <code title="object&#40;&#123;&#10; binary_authorization_policy &#61; optional&#40;string&#41;&#10; entry_point &#61; optional&#40;string, &#34;main&#34;&#41;&#10; instance_count &#61; optional&#40;number, 1&#41;&#10; memory_mb &#61; optional&#40;number, 256&#41; &#35; Memory in MB&#10; cpu &#61; optional&#40;string, &#34;0.166&#34;&#41;&#10; runtime &#61; optional&#40;string, &#34;python310&#34;&#41;&#10; timeout_seconds &#61; optional&#40;number, 180&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; entry_point &#61; &#34;main&#34;&#10; instance_count &#61; 1&#10; memory_mb &#61; 256&#10; cpu &#61; &#34;0.166&#34;&#10; runtime &#61; &#34;python310&#34;&#10; timeout_seconds &#61; 180&#10;&#125;">&#123;&#8230;&#125;</code> |
| [iam](variables.tf#L143) | IAM bindings for topic in {ROLE => [MEMBERS]} format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [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 . | <code>string</code> | | <code>null</code> |
| [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. | <code>string</code> | | <code>null</code> |
| [labels](variables.tf#L161) | Resource labels. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [prefix](variables.tf#L172) | Optional prefix used for resource names. | <code>string</code> | | <code>null</code> |
| [secrets](variables.tf#L192) | Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format. | <code title="map&#40;object&#40;&#123;&#10; is_volume &#61; bool&#10; project_id &#61; string&#10; secret &#61; string&#10; versions &#61; list&#40;string&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [service_account_config](variables-serviceaccount.tf#L17) | Service account configurations. | <code title="object&#40;&#123;&#10; create &#61; optional&#40;bool, true&#41;&#10; display_name &#61; optional&#40;string&#41;&#10; email &#61; optional&#40;string&#41;&#10; name &#61; optional&#40;string&#41;&#10; roles &#61; optional&#40;list&#40;string&#41;, &#91;&#10; &#34;roles&#47;logging.logWriter&#34;,&#10; &#34;roles&#47;monitoring.metricWriter&#34;&#10; &#93;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [trigger_config](variables.tf#L204) | Function trigger configuration. Leave null for HTTP trigger. | <code title="object&#40;&#123;&#10; event_type &#61; string&#10; pubsub_topic &#61; optional&#40;string&#41;&#10; region &#61; optional&#40;string&#41;&#10; event_filters &#61; optional&#40;list&#40;object&#40;&#123;&#10; attribute &#61; string&#10; value &#61; string&#10; operator &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;, &#91;&#93;&#41;&#10; service_account_email &#61; optional&#40;string&#41;&#10; service_account_create &#61; optional&#40;bool, false&#41;&#10; retry_policy &#61; optional&#40;string, &#34;RETRY_POLICY_DO_NOT_RETRY&#34;&#41; &#35; default to avoid permadiff&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [vpc_connector](variables.tf#L222) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | <code title="object&#40;&#123;&#10; create &#61; optional&#40;bool, false&#41;&#10; name &#61; optional&#40;string&#41;&#10; egress_settings &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [vpc_connector_config](variables.tf#L233) | VPC connector network configuration. Must be provided if new VPC connector is being created. | <code title="object&#40;&#123;&#10; ip_cidr_range &#61; string&#10; network &#61; string&#10; instances &#61; optional&#40;object&#40;&#123;&#10; max &#61; optional&#40;number&#41;&#10; min &#61; optional&#40;number, 2&#41;&#10; &#125;&#41;&#41;&#10; throughput &#61; optional&#40;object&#40;&#123;&#10; max &#61; optional&#40;number, 300&#41;&#10; min &#61; optional&#40;number, 200&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
## 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)
<!-- END TFDOC -->

View File

@@ -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))
)
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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 = {}
}

View File

@@ -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({

View File

@@ -35,7 +35,6 @@ counts:
google_cloudfunctions2_function: 1
google_storage_bucket: 1
google_storage_bucket_object: 1
modules: 1
resources: 6
outputs: {}

View File

@@ -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

View File

@@ -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: {}

View File

@@ -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

View File

@@ -22,4 +22,4 @@ counts:
google_cloudfunctions2_function: 2
google_storage_bucket_object: 2
modules: 2
resources: 7
resources: 13

View File

@@ -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

View File

@@ -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: {}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()