FAQ | This is a LIVE service | Changelog

Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • uis/devops/lib/ucam-faas-python
1 result
Show changes
Commits on Source (5)
Showing
with 801 additions and 49 deletions
...@@ -48,3 +48,24 @@ repos: ...@@ -48,3 +48,24 @@ repos:
hooks: hooks:
- id: mypy - id: mypy
additional_dependencies: ["types-decorator"] additional_dependencies: ["types-decorator"]
- repo: local
hooks:
# FIXME switch to use local logan settings file when pre-commit base image has logan installed
- id: terraform-fmt
name: terraform-fmt
language: docker_image
pass_filenames: false
entry: registry.gitlab.developers.cam.ac.uk/uis/devops/infra/dockerimages/logan-terraform:1.7 terraform fmt -recursive
- id: tflint
name: tflint
language: docker_image
pass_filenames: false
entry: ghcr.io/terraform-linters/tflint:latest
args: []
- id: trivy
name: trivy
language: docker_image
pass_filenames: false
entry: aquasec/trivy:latest
args: ["--cache-dir", "/tmp/.trivy-cache", "--skip-files", "example.Dockerfile", "--skip-dirs", "terraform/.terraform", "--skip-dirs", "tests", "config", ".", "--exit-code", "1"]
# Change Log
## [0.3.0]
### Added
- Crontab style schedule option added to terraform module
## [0.2.0]
### Added
- Additional example functions
- Basic exception handling for functions
### Changed
- Removed/renamed example functions
- Change test tooling function naming, `event_app_client` => `event_app_test_client_factory`
...@@ -10,6 +10,8 @@ ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 ...@@ -10,6 +10,8 @@ ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
WORKDIR /usr/lib WORKDIR /usr/lib
RUN adduser --system --no-create-home nonroot
# Pretty much everything from here on needs poetry. # Pretty much everything from here on needs poetry.
RUN pip install --no-cache-dir poetry RUN pip install --no-cache-dir poetry
...@@ -23,4 +25,9 @@ RUN poetry config virtualenvs.create false && \ ...@@ -23,4 +25,9 @@ RUN poetry config virtualenvs.create false && \
WORKDIR /usr/src/app WORKDIR /usr/src/app
USER nonroot
HEALTHCHECK --interval=5m --timeout=3s \
CMD curl -f http://localhost/healthy || exit 1
ENTRYPOINT ["ucam-faas", "--target"] ENTRYPOINT ["ucam-faas", "--target"]
# syntax = devthefuture/dockerfile-x # syntax = devthefuture/dockerfile-x
# We use the dockerfile-x syntax to demonstrate what would be required from an # We use the dockerfile-x syntax to demonstrate what would be required from an
# actual application that was using the base Dockerfile. Note that due to the # actual application that was using the base Dockerfile.
# docker build's context system this Dockerfile must be build from the parent
# directory, with the command:
#
# $ docker build -f example/Dockerfile .
FROM ./Dockerfile as base FROM ./Dockerfile as base
# Copy function code
COPY example ./example COPY example ./example
WORKDIR /usr/src/app/example WORKDIR /usr/src/app/example
...@@ -28,6 +23,5 @@ FROM base as full ...@@ -28,6 +23,5 @@ FROM base as full
# or, since poetry is available: # or, since poetry is available:
# RUN poetry install --no-root # RUN poetry install --no-root
# Set the CMD to execute the ucam-faas main code, with our target function. The # Set the CMD to the default function to call
# ENTRYPOINT is already set to 'sh -c'. CMD ["example_raw_event"]
CMD ["say_hello", "--debug"]
import random
from ucam_observe import get_structlog_logger
import ucam_faas import ucam_faas
logger = get_structlog_logger(__name__)
# This function is included to aid test observations
def patch_me(data):
pass
@ucam_faas.raw_event @ucam_faas.raw_event
def say_hello(event): def example_raw_event(raw_event):
return "hello world" logger.info("got_raw_event", raw_event=raw_event)
patch_me(raw_event)
@ucam_faas.cloud_event @ucam_faas.cloud_event
def receive_event(event): def example_cloud_event(cloud_event):
print(f"Received event data: {event}") logger.info("got_cloud_event", cloud_event=cloud_event)
patch_me(cloud_event)
@ucam_faas.cloud_event
def example_cloud_event_fail(cloud_event):
logger.info("got_cloud_event_fail", cloud_event=cloud_event)
patch_me(cloud_event)
raise ucam_faas.exceptions.UCAMFAASCouldNotProcess
@ucam_faas.cloud_event
def example_cloud_event_sometimes_fail(cloud_event):
logger.info("got_cloud_event_fail", cloud_event=cloud_event)
patch_me(cloud_event)
if bool(random.getrandbits(1)):
raise ucam_faas.exceptions.UCAMFAASCouldNotProcess
from main import receive_event, say_hello from unittest.mock import patch
from .main import example_cloud_event, example_raw_event
# Register the ucam_faas testing module as a pytest plugin - as it provides fixtures that we can # Register the ucam_faas testing module as a pytest plugin - as it provides fixtures that we can
# make use of when testing the functions. # make use of when testing the functions.
pytest_plugins = ["ucam_faas.testing"] pytest_plugins = ["ucam_faas.testing"]
def test_say_hello(): def test_example_raw_event():
""" """
Test the say_hello function using the undecorated version provided through the decorator Test the example_raw_event function using the undecorated version provided through the
library interface. decorator library interface.
This effectively tests the function 'raw', i.e. without any of the surrounding functions This effectively tests the function 'raw', i.e. without any of the surrounding functions
framework code in place. This example should be the primary way most tests are run for event framework code in place. This example should be the primary way most tests are run for event
functions, only in cases where the HTTP interaction is necessary should the app client be functions, only in cases where the HTTP interaction is necessary should the app client be
used. used.
""" """
assert say_hello.__wrapped__({}) == "hello world"
with patch("example.main.patch_me") as patched_patch_me:
assert example_raw_event.__wrapped__({}) is None
patched_patch_me.assert_called_once()
def test_client_say_hello(event_app_client): def test_client_example_raw_event(event_app_test_client_factory):
""" """
Test the say_hello function via a test HTTP client. Test the example_raw_event function via a test HTTP client.
This tests the function is responding in a HTTP environment. This test support is provided This tests the function is responding in a HTTP environment. This test support is provided
for verifying the function is registering as expected. Tests that can be written using the for verifying the function is registering as expected. Tests that can be written using the
'__wrapped__' interface should be preferred. '__wrapped__' interface should be preferred.
""" """
eac = event_app_client(target="say_hello", source="example/main.py") eac = event_app_test_client_factory(target="example_raw_event", source="example/main.py")
response = eac.get("/") response = eac.get("/")
assert response.status_code == 200 assert response.status_code == 200
def test_receive_event(capsys): def test_example_cloud_event():
receive_event.__wrapped__({"event": "yes!"}) with patch("example.main.patch_me") as patched_patch_me:
captured = capsys.readouterr() example_cloud_event.__wrapped__({"event": "yes!"})
assert captured.out == "Received event data: {'event': 'yes!'}\n"
patched_patch_me.assert_called_once_with({"event": "yes!"})
def test_client_receive_event(capsys, event_app_client):
eac = event_app_client(target="receive_event", source="example/main.py") def test_client_example_cloud_event(event_app_test_client_factory):
eac.post( eac = event_app_test_client_factory(target="example_cloud_event", source="example/main.py")
"/",
json={ # NOTE: When patching using event_app_test_client_factory, the module path will be relative
"specversion": "1.0", # to the source file
"type": "example.com.cloud.event", with patch("main.patch_me") as patched_patch_me:
"source": "https://example.com/cloudevents/pull", eac.post(
"subject": "123", "/",
"id": "A234-1234-1234", json={
"time": "2018-04-05T17:31:00Z", "specversion": "1.0",
"data": {"event": "yes!"}, "type": "example.com.cloud.event",
}, "source": "https://example.com/cloudevents/pull",
) "subject": "123",
captured = capsys.readouterr() "id": "A234-1234-1234",
assert captured.out == "Received event data: {'event': 'yes!'}\n" "time": "2018-04-05T17:31:00Z",
"data": {"event": "yes!"},
},
def test_status_and_healthy(event_app_client): )
eac = event_app_client(target="say_hello", source="example/main.py")
patched_patch_me.assert_called_once()
assert patched_patch_me.call_args.args[0] == {"event": "yes!"}
def test_status_and_healthy(event_app_test_client_factory):
eac = event_app_test_client_factory(target="example_raw_event", source="example/main.py")
response = eac.get("/healthy") response = eac.get("/healthy")
assert response.status_code == 200 assert response.status_code == 200
assert response.text == "ok" assert response.text == "ok"
......
[tool.poetry] [tool.poetry]
name = "ucam-faas" name = "ucam-faas"
version = "0.1.3" version = "0.3.0"
description = "Opinionated FaaS support framework extending Google's functions-framework" description = "Opinionated FaaS support framework extending Google's functions-framework"
authors = ["University of Cambridge Information Services <devops-wilson@uis.cam.ac.uk>"] authors = ["University of Cambridge Information Services <devops-wilson@uis.cam.ac.uk>"]
readme = "README.md" readme = "README.md"
...@@ -57,7 +57,22 @@ cmd = "tox" ...@@ -57,7 +57,22 @@ cmd = "tox"
[tool.poe.tasks."pytest:local"] [tool.poe.tasks."pytest:local"]
help = "Run the Python test suite via pytest using the locally installed Python version" help = "Run the Python test suite via pytest using the locally installed Python version"
cmd = "pytest example/tests.py" cmd = "pytest"
[tool.poe.tasks."terraform:auth"]
cmd = "gcloud auth application-default login"
[tool.poe.tasks."terraform:dev:init"]
cmd = "logan --workspace=development terraform init"
cwd = "terraform"
[tool.poe.tasks."terraform:dev:apply"]
cmd = "logan --workspace=development terraform apply"
cwd = "terraform"
[tool.poe.tasks."terraform:dev"]
cmd = "logan --workspace=development terraform"
cwd = "terraform"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.9.1" python = "^3.9.1"
......
root=true
[Dockerfile]
indent_style=space
indent_size=2
[*.{tf,tfvars}]
indent_style=space
indent_size=2
[*.md]
indent_style=space
indent_size=4
[*.{yaml,yml}]
indent_style=space
indent_size=2
[*.py]
max_line_length=99
[*.sql]
indent_style=space
indent_size=2
.terraform/
terraform.tfstate.*.backup
terraform.tfstate.d/
# Editor files
.idea
# Mac DS_Store files
.DS_Store
# vscode files
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# .logan.yaml configures the logan tool to run terraform
version: "1.2"
project: "ucam-faas"
image: "registry.gitlab.developers.cam.ac.uk/uis/devops/infra/dockerimages/logan-terraform:1.7"
mount_docker_socket: true
use_impersonation: true
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/gitlabhq/gitlab" {
version = "16.11.0"
constraints = "~> 16.11"
hashes = [
"h1:Hw1BJh3S2wedHr5geP6YBzyfeFwvI/KsEsAr7Box3F0=",
"h1:dyxmqEcBEzd5VS853KsG/oZo700KSIXDV7XSL+p0vGc=",
"zh:00e6d68c97c739f320407f76537e58218b52cea0128c01a8995820be79859810",
"zh:14578344dc44043f537c248e04da766b79587f47327e0dfe7dac4e9e272b7c49",
"zh:26817d48b62907fe1cc16cface8d6035395c9370dfc39e2fbb1ee7a112c10584",
"zh:28ad3bdedd76cb7e2290a0b1a2288c5042913d46879644a6776a0fe3e109db12",
"zh:3882e3d81e751074bf0ae2ee2008c058d6b5b797e8d3f7582c041f7657404c2d",
"zh:402eda34a8759246f084936bdd511073abb79337ce879a5bba46c065912028f3",
"zh:6686b2a58e973b570204c63f83f9d5bb7f641406177857fe05619c5679ffda05",
"zh:8be6c674904b5954d51510663cc74e9d03ec7ee500f0e0e234fe85d9d7d7500c",
"zh:a0813c74a396d14be8332dffad9f597705af1246bb9b582f149d00c86ad8e24a",
"zh:bc782bf60c3956c61f52e0c90ab9c125e1c49f2472173ca7edf0bf99fadb05ac",
"zh:cd9b0e82d2f14e69347c9bb2ecc8fec67238b565bd0f5f5bde4020b98af09e93",
"zh:ef7582fee9d96d52be020512180801c18e5d4589b4e31588c235c191acbd9ddc",
"zh:f6f4c313cc90994b122bf0af206d225125d0d7818972b74949944038d8602941",
"zh:f712c56d17de482a6b4a43ce81d25937a8c9e882e1aa6161f413ab250ae84e65",
"zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32",
]
}
provider "registry.terraform.io/hashicorp/google" {
version = "5.28.0"
constraints = "~> 5.0"
hashes = [
"h1:bZDLXCLRyFQYTySa+H6gwFEeHOiCh/9YKOKI+Wm1VEo=",
"h1:moM2ZvsEVjlowEJpUKC48irzrEerzWoJNBgeAd73s/k=",
"zh:00fb6916789d56c8801f95624fd30aca05f47918e6fab5c05fab7214cdecfc65",
"zh:204cc06787b8c55d2db744d020edf98bfdf294ed0a5d0fdc272afc0a9568a102",
"zh:3ccc7337801b6ebc8362a3cf4ae8eafacd235ee2389c84a58a4a7a6878159687",
"zh:6a91cf54404112651a2cffa2d59a140f1b1dbff7ff12e875f154deaebd969500",
"zh:6ade8996b11edb74afdf2b1b6c39c817e7f62bf2e899b1831bbc740457780456",
"zh:8691ad4285bf41a054a715b0cb9eb32c919512dded081437314b506fbe1ad0d2",
"zh:9c2ff4ca96299f65a6d23bb08d2a5f7005bef180fe5c9a3b5b3577f381d8bc8a",
"zh:cda256ff269b7ae289059e93f4d0ed071689c3fe58dcf6b3b68011523fc37c2d",
"zh:e38dc30b722922240c54ad2164a80505698933220afb2cde86b654cfc8e28900",
"zh:e3f8c05fc51a85508d78e3c5269d1b1c8425fe7c35f2659532d19be8da18c0ce",
"zh:e4894e409fcfbe0148e325ec9d342b4f3cf86b313e165628d20f90311e117a1d",
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
]
}
provider "registry.terraform.io/hashicorp/google-beta" {
version = "5.28.0"
constraints = "~> 5.0"
hashes = [
"h1:AakwLx3qzcM6ze+b734Di0piLw/Bof8mkJeYHxGhRQM=",
"h1:BrQWd1Hyqta8VI23yaCeIGDVxmyFoTyIBizHWTdkj0U=",
"zh:2cfb876235f0fbad55ef38389ab61daa3bee0b98d63b7131f4809775e2f4f824",
"zh:5bd207c00b53200fc6046ac61bafd96fc2fa36c0fe796a261c3958921238121d",
"zh:603dc0c7728e3c159bd6383eb08e0fe2a2fa4f872671102fbecb956279eed6a4",
"zh:6346401ade115f53eee296077ae9740f7ed790a59292fe01a7d11c4d4ff10f93",
"zh:661dfc3bd710291657ae4e8c472c78ae6025a477c125634952c27401ad675e61",
"zh:8257afe599ef95f3821d4f993349b661192b7e4ff2d0df32e2e62f8ba3a4c306",
"zh:c7903e573367b8d6b6cc389d2eef34536191900dd198b904a41fd1d682bf863f",
"zh:d7f061c59156489da24d80ca72b28330792e981d25b245a2e15825317c85b41c",
"zh:e6c67b4390a01cb97610dcc8cbfad8ab6225fbc6e2b5d3d02ebd0f6d8f66341e",
"zh:eb1db70a359bbf32d26e9fd621320b68791700636fded4120e82c3d9de8a18c9",
"zh:f270d24e135fc38e655f0be05b3223ab29b937fbd9f2f01e9e72dffc67e7a660",
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
]
}
provider "registry.terraform.io/hashicorp/random" {
version = "3.6.1"
constraints = "~> 3.6"
hashes = [
"h1:12+TxYsSS5bzT7uiE2w0ke2WxmhehRV7uKU1wKUUnmM=",
"h1:a+Goawwh6Qtg4/bRWzfDtIdrEFfPlnVy0y4LdUQY3nI=",
"zh:2a0ec154e39911f19c8214acd6241e469157489fc56b6c739f45fbed5896a176",
"zh:57f4e553224a5e849c99131f5e5294be3a7adcabe2d867d8a4fef8d0976e0e52",
"zh:58f09948c608e601bd9d0a9e47dcb78e2b2c13b4bda4d8f097d09152ea9e91c5",
"zh:5c2a297146ed6fb3fe934c800e78380f700f49ff24dbb5fb5463134948e3a65f",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:7ce41e26f0603e31cdac849085fc99e5cd5b3b73414c6c6d955c0ceb249b593f",
"zh:8c9e8d30c4ef08ee8bcc4294dbf3c2115cd7d9049c6ba21422bd3471d92faf8a",
"zh:93e91be717a7ffbd6410120eb925ebb8658cc8f563de35a8b53804d33c51c8b0",
"zh:982542e921970d727ce10ed64795bf36c4dec77a5db0741d4665230d12250a0d",
"zh:b9d1873f14d6033e216510ef541c891f44d249464f13cc07d3f782d09c7d18de",
"zh:cfe27faa0bc9556391c8803ade135a5856c34a3fe85b9ae3bdd515013c0c87c1",
"zh:e4aabf3184bbb556b89e4b195eab1514c86a2914dd01c23ad9813ec17e863a8a",
]
}
plugin "terraform" {
enabled = true
preset = "all"
}
# backend.tf configures the teraform remote state backend.
terraform {
backend "gcs" {
# This bucket has been created by the Powers That Be for our use.
bucket = "ucam-faas-config-09574972"
prefix = "terraform/ucam-faas"
# Product-wide terraform-state service account. This value must hard-coded.
impersonate_service_account = "terraform-state@ucam-faas-meta-5e4d12ab.iam.gserviceaccount.com"
}
}
# config.tf provides access to the configuration for the product provided by DevOps Cloud Team.
# Machine-readable configuration is provided to us as a storage object.
data "google_storage_bucket_object_content" "configuration_json" {
bucket = local.config_bucket
name = local.config_path
# The config json file is read by your personal user account. Speak to the DevOps Cloud Team if you require access.
provider = google.impersonation
}
locals {
gcp_config = jsondecode(data.google_storage_bucket_object_content.configuration_json.content)
workspace_config = local.gcp_config.workspaces[terraform.workspace]
}
resource "google_pubsub_topic" "faas_test" {
name = "faas-test"
project = local.project
}
module "faas_service" {
source = "./modules/faas"
name = "faas-test-pubsub"
project = local.project
function_container_image = "${local.container_images.function_base}:${local.container_images.function_tag}"
function = "example_cloud_event"
timeout_seconds = 10
triggers = {
pubsub_topic_id = google_pubsub_topic.faas_test.id
}
function_env = [
{
name = "TEST_VAR"
value = "WORKING"
}
]
concurrency = {
max_concurrent_functions = 1
}
}
module "faas_service_set_retry" {
source = "./modules/faas"
name = "faas-test-set-retry"
project = local.project
function_container_image = "${local.container_images.function_base}:${local.container_images.function_tag}"
function = "example_cloud_event"
timeout_seconds = 20
triggers = {
pubsub_topic_id = google_pubsub_topic.faas_test.id
}
retry_count = 4
function_env = [
{
name = "TEST_VAR"
value = "WORKING"
}
]
concurrency = {
max_concurrent_functions = 1
}
}
module "faas_service_cron" {
source = "./modules/faas"
name = "faas-test-cron"
project = local.project
function_container_image = "${local.container_images.function_base}:${local.container_images.function_tag}"
function = "example_cloud_event"
timeout_seconds = 20
triggers = {
cron_schedules = ["00 0 */7 * *", "0 12 */7 * *", ]
}
retry_count = 4
function_env = [
{
name = "TEST_VAR"
value = "WORKING"
}
]
concurrency = {
max_concurrent_functions = 1
}
}
# locals.tf contain definitions for local variables which are of general utility
# within the configuration.
# These locals are provided by the DevOps Cloud Team.
locals {
# Bucket and path for GCP-admin provided configuration.
config_bucket = "ucam-faas-config-09574972"
config_path = "config_v1.json"
}
# These locals come from the manual bootstrapping steps.
locals {
# Base URLs of GitLab instance holding deployment and webapp projects.
gitlab_base_url = "https://gitlab.developers.cam.ac.uk/"
# Name of Secret Manager secret which contains the GitLab access token for
# this deployment.
gitlab_access_token_secret_name = "gitlab-access-token"
# Project which contains the GitLab access token secret.
gitlab_access_token_secret_project = local.product_meta_project
notification_channels = [local.gcp_config.notification_channels.email.wilson_team]
# Name of secret in meta project containing workspace-specific secrets. See
# the README for the format of this secret. If blank, no workspace-specific
# secrets are used.
workspace_extra_secrets_secret_name = ""
}
# This data source retrieves the webapp project from GitLab based on the project id
# specified when running copier. This is used in the following locals section.
data "gitlab_project" "faas" {
id = "8520"
}
# These locals define common configuration parameters for the deployment.
locals {
# Default region for resources.
region = "europe-west2"
# Container images used in this deployment. Generally these
# should be tagged explicitly with the Git commit SHA for the exact version to
# deploy. They are specified per-workspace with generic "latest from master"
# fallbacks if not otherwise specified.
container_images = lookup({
development = {
function_base = join("/", [
local.gcp_config.artifact_registry_docker_repository,
data.gitlab_project.faas.path,
"example",
"main"
])
function_tag = "latest"
}
}, terraform.workspace, {})
}
# These locals are derived from resources, data sources or other locals.
locals {
# Project id of product-specific meta project.
product_meta_project = local.gcp_config.product_meta_project
# Project id for workspace-specific project.
project = local.workspace_config.project_id
}
# FIXME Currently subscription and cloud service account are shared
resource "google_service_account" "cloud_run_sa" {
project = var.project
account_id = "${var.name}-cloud-run"
}
resource "google_cloud_run_service_iam_binding" "binding" {
location = var.location
service = var.name
role = "roles/run.invoker"
members = ["serviceAccount:${google_service_account.cloud_run_sa.email}"]
}
resource "google_project_service_identity" "pubsub_agent" {
provider = google-beta
project = var.project
service = "pubsub.googleapis.com"
}
# This only providing access to a role to the service account/pubsub
#trivy:ignore:AVD-GCP-0011
resource "google_project_iam_binding" "project_token_creator" {
project = var.project
role = "roles/iam.serviceAccountTokenCreator"
members = ["serviceAccount:${google_project_service_identity.pubsub_agent.email}"]
}
resource "google_cloud_run_v2_service" "faas" {
name = var.name
location = var.location
template {
service_account = google_service_account.cloud_run_sa.email
containers {
image = var.function_container_image
args = var.function != null ? [var.function] : null
dynamic "env" {
for_each = var.function_env
content {
name = env.value["name"]
value = env.value["value"]
dynamic "value_source" {
for_each = env.value["value_source"] != null ? [env.value["value_source"]] : []
content {
dynamic "secret_key_ref" {
for_each = value_source.value["secret_key_ref"] != null ? [value_source.value["secret_key_ref"]] : []
content {
secret = secret_key_ref.value["secret"]
version = secret_key_ref.value["version"]
}
}
}
}
}
}
liveness_probe {
http_get {
path = "/healthy"
}
}
startup_probe {
http_get {
path = "/healthy"
}
}
}
scaling {
min_instance_count = 0
max_instance_count = var.concurrency.max_concurrent_functions > var.concurrency.max_container_concurrency ? (var.concurrency.max_concurrent_functions - (var.concurrency.max_concurrent_functions % var.concurrency.max_container_concurrency)) / var.concurrency.max_container_concurrency : 1
}
max_instance_request_concurrency = var.concurrency.max_concurrent_functions > var.concurrency.max_container_concurrency ? var.concurrency.max_container_concurrency : var.concurrency.max_concurrent_functions
dynamic "vpc_access" {
for_each = var.vpc_access != null ? [var.vpc_access] : []
content {
connector = vpc_access.value["connector"]
egress = vpc_access.value["egress"]
dynamic "network_interfaces" {
for_each = vpc_access.value["network_interfaces"] != null ? [vpc_access.value["network_interfaces"]] : []
iterator = network_interface
content {
network = network_interface.value["network"]
subnetwork = network_interface.value["subnetwork"]
}
}
}
}
}
}
locals {
cron_schedules_supplied = length(var.triggers.cron_schedules) > 0
}
resource "google_pubsub_subscription" "main" {
# Set count to aid adding non-pubsub based trigger if needed in the future
count = 1
name = var.name
topic = local.cron_schedules_supplied ? google_pubsub_topic.scheduler_pubsub[0].id : var.triggers.pubsub_topic_id
push_config {
push_endpoint = google_cloud_run_v2_service.faas.uri
oidc_token {
service_account_email = google_service_account.cloud_run_sa.email
}
attributes = {
x-goog-version = "v1"
}
}
dead_letter_policy {
dead_letter_topic = google_pubsub_topic.dead_letter.id
max_delivery_attempts = var.retry_count + 1
}
retry_policy {
minimum_backoff = "30s"
maximum_backoff = "600s"
}
expiration_policy {
ttl = "" # Never expire
}
ack_deadline_seconds = var.timeout_seconds
}
resource "google_pubsub_topic" "scheduler_pubsub" {
count = local.cron_schedules_supplied ? 1 : 0
name = "${var.name}-cron"
project = var.project
}
resource "google_cloud_scheduler_job" "cron" {
for_each = { for index, cron in var.triggers.cron_schedules : index => cron }
name = "${var.name}-${each.key}"
project = var.project
schedule = each.value
time_zone = "Europe/London"
pubsub_target {
topic_name = google_pubsub_topic.scheduler_pubsub[0].id
data = base64encode(jsonencode({ "cron_schedule" : each.value }))
}
}
# FIXME when deadletter functionality added, ensure subscription exists
# Currently just used to provider retry count control
resource "google_pubsub_topic" "dead_letter" {
name = "${var.name}-dl"
project = var.project
}
#output "dead_letter_topic_id" {
# value = google_pubsub_topic.dead_letter.id
# description = "The ID of the dead letter Pub/Sub topic."
#}
output "service" {
description = "Webapp Cloud Run service resource"
value = google_cloud_run_v2_service.faas
}
output "service_account" {
description = "Service account which service runs as"
value = google_service_account.cloud_run_sa
}
variable "name" {
type = string
description = "The name of the Cloud Run service."
}
variable "function_container_image" {
type = string
description = "The container image with the function."
}
variable "location" {
type = string
description = "The location where the service will be deployed."
default = "europe-west2"
}
variable "project" {
type = string
description = "The ID of the project where the resources will be deployed."
}
variable "function" {
type = string
description = "Override default function to call. Useful when a container has multiple functions."
default = null
}
variable "function_env" {
type = list(object({
name = string
value = optional(string)
value_source = optional(object({
secret_key_ref = optional(object({
secret = string
version = optional(string, "latest")
}))
}))
}))
default = []
}
variable "concurrency" {
type = object({
max_concurrent_functions = optional(number, 80)
max_container_concurrency = optional(number, 8)
})
default = {
}
validation {
condition = (var.concurrency.max_concurrent_functions % var.concurrency.max_container_concurrency) == 0 || (var.concurrency.max_concurrent_functions < var.concurrency.max_container_concurrency)
error_message = "max_concurrent_functions must be an increment of max_container_concurrency (or less than in which case max_container_concurrency is ignored)"
}
description = <<EOI
Options that determine concurrency of the function.
max_concurrent_functions - default 80 - total concurrent instances of the function across all containers.
Must be an increment of max_container_concurrency (or less than in which case max_container_concurrency is ignored)
max_container_concurrency - default 8 - maximum concurrent functions per container.
EOI
}
variable "retry_count" {
type = number
default = 4
description = "Number of attempts to retry a job."
validation {
condition = var.retry_count >= 4
error_message = "retry_count >= 4"
}
}
variable "timeout_seconds" {
type = number
description = "Maximum duration of the function before it timeouts and marked as failed."
}
variable "vpc_access" {
description = <<EOI
Configure VPC access for the Cloud Run service. For more information on these
options see
https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloud_run_v2_service#nested_vpc_access
EOI
type = object({
connector = optional(string)
egress = optional(string)
network_interfaces = optional(object({
network = optional(string)
subnetwork = optional(string)
tags = optional(string)
}))
})
default = null
}
variable "triggers" {
type = object({
pubsub_topic_id = optional(string, null)
cron_schedules = optional(list(string), [])
})
description = <<EOI
pubsub_topic_id - PUB/SUB topic ID to subscribe to and trigger function from.
cron_schedules - list of crontab style scheduler strings, e.g. ["30 16 * * 7"]
See http://man7.org/linux/man-pages/man5/crontab.5.html
EOI
validation {
condition = var.triggers.pubsub_topic_id != null || length(var.triggers.cron_schedules) > 0
error_message = "either pubsub_topic_id must be set or cron_schedules must be of length > 0"
}
validation {
condition = var.triggers.pubsub_topic_id == null || length(var.triggers.cron_schedules) < 1
error_message = "only one of pubsub_topic_id or cron_schedules must be set"
}
}