From 4eafea7a1380830b1346ca4bbeaea308410d28ee Mon Sep 17 00:00:00 2001 From: Alastair Lock Date: Mon, 29 Dec 2025 15:53:42 +0000 Subject: [PATCH 1/4] Ppha-810: Create slack integration into the pipeline --- infrastructure/bootstrap/hub.bicep | 5 +++++ infrastructure/modules/infra/data.tf | 5 +++++ infrastructure/modules/infra/logic_app.tf | 22 ++++++++++++++++++++++ scripts/terraform/terraform.mk | 1 + 4 files changed, 33 insertions(+) create mode 100644 infrastructure/modules/infra/logic_app.tf diff --git a/infrastructure/bootstrap/hub.bicep b/infrastructure/bootstrap/hub.bicep index b17eaa81..ff9810ea 100644 --- a/infrastructure/bootstrap/hub.bicep +++ b/infrastructure/bootstrap/hub.bicep @@ -18,6 +18,7 @@ targetScope = 'subscription' +// param devopsInfrastructureId string param devopsSubnetAddressPrefix string param privateEndpointSubnetAddressPrefix string param hubType string // live / nonlive @@ -25,6 +26,10 @@ param region string = 'uksouth' param regionShortName string = 'uks' param vnetAddressPrefixes array param enableSoftDelete bool +<<<<<<< HEAD +======= + +>>>>>>> 93647fb (wip) // removed when generalised var appShortName = 'lungcs' diff --git a/infrastructure/modules/infra/data.tf b/infrastructure/modules/infra/data.tf index 7e9605eb..03851750 100644 --- a/infrastructure/modules/infra/data.tf +++ b/infrastructure/modules/infra/data.tf @@ -21,3 +21,8 @@ data "azurerm_key_vault_secret" "infra" { name = "monitoring-email-address" key_vault_id = data.azurerm_key_vault.infra.id } + +data "azurerm_key_vault_secret" "slack_webhook_url" { + name = "slack-webhook-url" + key_vault_id = data.azurerm_key_vault.infra.id +} diff --git a/infrastructure/modules/infra/logic_app.tf b/infrastructure/modules/infra/logic_app.tf new file mode 100644 index 00000000..6d210751 --- /dev/null +++ b/infrastructure/modules/infra/logic_app.tf @@ -0,0 +1,22 @@ +module "logic_app_slack_alert" { + # TODO: Add this back in before merging into main + # count = var.enable_alerting ? 1 : 0 + source = "../dtos-devops-templates/infrastructure/modules/logic-app-slack-alert" + + name = "logic-${var.app_short_name}-${var.environment}-slack-alerts" + resource_group_name = azurerm_resource_group.main.name + location = var.region + slack_webhook_url = data.azurerm_key_vault_secret.slack_webhook_url.value +} + +resource "azurerm_monitor_action_group" "slack" { + name = "ag-slack-myapp-dev-uks" + resource_group_name = azurerm_resource_group.main.name + short_name = "slack" + + webhook_receiver { + name = "logic-app-slack" + service_uri = module.logic_app_slack_alert.trigger_callback_url + use_common_alert_schema = true + } +} diff --git a/scripts/terraform/terraform.mk b/scripts/terraform/terraform.mk index f3b45da1..38f7862f 100644 --- a/scripts/terraform/terraform.mk +++ b/scripts/terraform/terraform.mk @@ -30,6 +30,7 @@ prod: # Target the prod environment - make prod review: # Target the review infrastructure, or a review app if PR_NUMBER is used - make review [PR_NUMBER=] $(eval include infrastructure/environments/review/variables.sh) $(if ${PR_NUMBER}, $(eval export TF_VAR_deploy_infra=false), $(eval export TF_VAR_deploy_container_apps=true)) + $(if ${PR_NUMBER},,$(eval export TF_VAR_deploy_container_apps=false)) $(if ${PR_NUMBER}, $(eval export ENVIRONMENT=pr-${PR_NUMBER}), $(eval export ENVIRONMENT=review)) db-setup: From f71399af93fddbdc4934522f9a9ea585ea23fb76 Mon Sep 17 00:00:00 2001 From: Alastair Lock Date: Tue, 28 Apr 2026 16:43:41 +0100 Subject: [PATCH 2/4] request summary command --- infrastructure/bootstrap/hub.bicep | 5 -- infrastructure/modules/container-apps/jobs.tf | 64 +++++++++++++++++++ .../questions/management/__init__.py | 0 .../questions/management/commands/__init__.py | 0 .../management/commands/request_summary.py | 21 ++++++ .../questions/services/__init__.py | 0 .../questions/services/request_summary.py | 23 +++++++ 7 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 lung_cancer_screening/questions/management/__init__.py create mode 100644 lung_cancer_screening/questions/management/commands/__init__.py create mode 100644 lung_cancer_screening/questions/management/commands/request_summary.py create mode 100644 lung_cancer_screening/questions/services/__init__.py create mode 100644 lung_cancer_screening/questions/services/request_summary.py diff --git a/infrastructure/bootstrap/hub.bicep b/infrastructure/bootstrap/hub.bicep index ff9810ea..b17eaa81 100644 --- a/infrastructure/bootstrap/hub.bicep +++ b/infrastructure/bootstrap/hub.bicep @@ -18,7 +18,6 @@ targetScope = 'subscription' -// param devopsInfrastructureId string param devopsSubnetAddressPrefix string param privateEndpointSubnetAddressPrefix string param hubType string // live / nonlive @@ -26,10 +25,6 @@ param region string = 'uksouth' param regionShortName string = 'uks' param vnetAddressPrefixes array param enableSoftDelete bool -<<<<<<< HEAD -======= - ->>>>>>> 93647fb (wip) // removed when generalised var appShortName = 'lungcs' diff --git a/infrastructure/modules/container-apps/jobs.tf b/infrastructure/modules/container-apps/jobs.tf index 8770825d..ee723a24 100644 --- a/infrastructure/modules/container-apps/jobs.tf +++ b/infrastructure/modules/container-apps/jobs.tf @@ -1,3 +1,16 @@ +locals { + scheduled_jobs = { + collect_metrics = { + cron_expression = "0 */6 * * *" + environment_variables = { + ENVIRONMENT = var.environment + } + job_short_name = "rs" + job_container_args = "request_summary" + } + } +} + module "db_setup" { source = "../dtos-devops-templates/infrastructure/modules/container-app-job" @@ -25,3 +38,54 @@ module "db_setup" { ] } + +module "scheduled_jobs" { + source = "../dtos-devops-templates/infrastructure/modules/container-app-job" + + for_each = local.scheduled_jobs + + name = "${var.app_short_name}-${each.value.job_short_name}-${var.environment}" + container_app_environment_id = var.container_app_environment_id + resource_group_name = azurerm_resource_group.main.name + + fetch_secrets_from_app_key_vault = var.fetch_secrets_from_app_key_vault + app_key_vault_id = var.app_key_vault_id + + container_command = ["/bin/sh", "-c"] + container_args = [ + "python manage.py ${each.value.job_container_args}" + ] + + docker_image = var.docker_image + replica_retry_limit = 0 + user_assigned_identity_ids = flatten([ + [module.azure_blob_storage_identity.id], + var.deploy_database_as_container ? [] : [module.db_connect_identity[0].id] + ]) + + environment_variables = merge( + local.common_env, + { + "STORAGE_ACCOUNT_NAME" = module.storage.storage_account_name, + "BLOB_MI_CLIENT_ID" = module.azure_blob_storage_identity.client_id, + }, + each.value.environment_variables, + var.deploy_database_as_container ? local.container_db_env : local.azure_db_env + ) + secret_variables = merge( + # { APPLICATIONINSIGHTS_CONNECTION_STRING = var.app_insights_connection_string }, + var.deploy_database_as_container ? { DATABASE_PASSWORD = resource.random_password.admin_password[0].result } : {} + ) + + # alerts + action_group_id = var.action_group_id + enable_alerting = var.enable_alerting + log_analytics_workspace_id = var.log_analytics_workspace_audit_id + + # Ensure RBAC role assignments are created before the job definition finalizes + depends_on = [ + module.blob_storage_role_assignment, + ] + + cron_expression = each.value.cron_expression +} diff --git a/lung_cancer_screening/questions/management/__init__.py b/lung_cancer_screening/questions/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lung_cancer_screening/questions/management/commands/__init__.py b/lung_cancer_screening/questions/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lung_cancer_screening/questions/management/commands/request_summary.py b/lung_cancer_screening/questions/management/commands/request_summary.py new file mode 100644 index 00000000..6d22a67b --- /dev/null +++ b/lung_cancer_screening/questions/management/commands/request_summary.py @@ -0,0 +1,21 @@ +import logging + +from django.core.management.base import BaseCommand, CommandError +from lung_cancer_screening.questions.services.request_summary import RequestSummary + +logger = logging.getLogger(__name__) + +class Command(BaseCommand): + help = "Counts the number of submitted requests." + + def handle(self, *args, **options): + logger.info("Command: SubmittedCount.") + try: + rs = RequestSummary() + summary = rs.get_summary() + + self.stdout.write(str(summary)) + + except Exception as e: + logger.error(e, exc_info=True) + raise CommandError(e) diff --git a/lung_cancer_screening/questions/services/__init__.py b/lung_cancer_screening/questions/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lung_cancer_screening/questions/services/request_summary.py b/lung_cancer_screening/questions/services/request_summary.py new file mode 100644 index 00000000..7161cd7a --- /dev/null +++ b/lung_cancer_screening/questions/services/request_summary.py @@ -0,0 +1,23 @@ +import logging +from ..models.response_set import ResponseSet + +logger = logging.getLogger(__name__) + +class RequestSummary: + + def __init__(self): + logger.info("RequestSummary: init") + + def get_submitted_count(self): + + return ResponseSet.objects.submitted().count() + + def get_count(self): + + return ResponseSet.objects.count() + + def get_summary(self): + return { + "total": self.get_count(), + "submitted": self.get_submitted_count(), + } From b444a32bce61ba419d134419aac14d5302695b15 Mon Sep 17 00:00:00 2001 From: Alastair Lock Date: Wed, 29 Apr 2026 13:54:48 +0100 Subject: [PATCH 3/4] adding in webhook --- infrastructure/modules/container-apps/jobs.tf | 1 + .../modules/container-apps/variables.tf | 5 ++++ infrastructure/modules/infra/data.tf | 5 ---- infrastructure/modules/infra/logic_app.tf | 2 +- infrastructure/modules/infra/variables.tf | 5 ++++ infrastructure/terraform/spoke/data.tf | 14 +++++++++++ infrastructure/terraform/spoke/main.tf | 2 ++ .../management/commands/request_summary.py | 25 ++++++++++++++++++- 8 files changed, 52 insertions(+), 7 deletions(-) diff --git a/infrastructure/modules/container-apps/jobs.tf b/infrastructure/modules/container-apps/jobs.tf index ee723a24..7681025f 100644 --- a/infrastructure/modules/container-apps/jobs.tf +++ b/infrastructure/modules/container-apps/jobs.tf @@ -74,6 +74,7 @@ module "scheduled_jobs" { ) secret_variables = merge( # { APPLICATIONINSIGHTS_CONNECTION_STRING = var.app_insights_connection_string }, + { SLACK_WEBHOOK_URL = var.slack_webhook_url }, var.deploy_database_as_container ? { DATABASE_PASSWORD = resource.random_password.admin_password[0].result } : {} ) diff --git a/infrastructure/modules/container-apps/variables.tf b/infrastructure/modules/container-apps/variables.tf index 5c752459..bf3137c9 100644 --- a/infrastructure/modules/container-apps/variables.tf +++ b/infrastructure/modules/container-apps/variables.tf @@ -196,6 +196,11 @@ variable "infra_key_vault_rg" { type = string } +variable "slack_webhook_url" { + description = "slack_webhook_url is the URL used to send alerts to Slack. It should be stored as a secret in the infra key vault with the name 'slack-webhook-url'." + type = string +} + locals { resource_group_name = "rg-${var.app_short_name}-${var.environment}-container-app-uks" diff --git a/infrastructure/modules/infra/data.tf b/infrastructure/modules/infra/data.tf index 03851750..7e9605eb 100644 --- a/infrastructure/modules/infra/data.tf +++ b/infrastructure/modules/infra/data.tf @@ -21,8 +21,3 @@ data "azurerm_key_vault_secret" "infra" { name = "monitoring-email-address" key_vault_id = data.azurerm_key_vault.infra.id } - -data "azurerm_key_vault_secret" "slack_webhook_url" { - name = "slack-webhook-url" - key_vault_id = data.azurerm_key_vault.infra.id -} diff --git a/infrastructure/modules/infra/logic_app.tf b/infrastructure/modules/infra/logic_app.tf index 6d210751..64566b3a 100644 --- a/infrastructure/modules/infra/logic_app.tf +++ b/infrastructure/modules/infra/logic_app.tf @@ -6,7 +6,7 @@ module "logic_app_slack_alert" { name = "logic-${var.app_short_name}-${var.environment}-slack-alerts" resource_group_name = azurerm_resource_group.main.name location = var.region - slack_webhook_url = data.azurerm_key_vault_secret.slack_webhook_url.value + slack_webhook_url = var.slack_webhook_url } resource "azurerm_monitor_action_group" "slack" { diff --git a/infrastructure/modules/infra/variables.tf b/infrastructure/modules/infra/variables.tf index 36726480..f845c75f 100644 --- a/infrastructure/modules/infra/variables.tf +++ b/infrastructure/modules/infra/variables.tf @@ -73,6 +73,11 @@ variable "enable_alerting" { type = bool } +variable "slack_webhook_url" { + description = "slack_webhook_url is the URL used to send alerts to Slack. It should be stored as a secret in the infra key vault with the name 'slack-webhook-url'." + type = string +} + locals { hub_vnet_rg_name = "rg-hub-${var.hub}-uks-bootstrap" hub_vnet_name = "vnet-hub-${var.hub}-uks" diff --git a/infrastructure/terraform/spoke/data.tf b/infrastructure/terraform/spoke/data.tf index 855734fc..c0cfc9e5 100644 --- a/infrastructure/terraform/spoke/data.tf +++ b/infrastructure/terraform/spoke/data.tf @@ -49,3 +49,17 @@ data "azurerm_application_insights" "app_insights" { name = "appi-${var.env_config}-uks-${var.app_short_name}" resource_group_name = local.resource_group_name } + +data "azurerm_key_vault" "infra" { + provider = azurerm.hub + + name = local.infra_key_vault_name + resource_group_name = local.infra_key_vault_rg +} + +data "azurerm_key_vault_secret" "slack_webhook_url" { + name = "slack-webhook-url" + key_vault_id = data.azurerm_key_vault.infra.id +} + +# git-sha-a85180497c23d742b5f92262b3b43069e44a4110 diff --git a/infrastructure/terraform/spoke/main.tf b/infrastructure/terraform/spoke/main.tf index d3626dd8..ee141137 100644 --- a/infrastructure/terraform/spoke/main.tf +++ b/infrastructure/terraform/spoke/main.tf @@ -22,6 +22,7 @@ module "infra" { vnet_address_space = var.vnet_address_space cae_zone_redundancy_enabled = var.cae_zone_redundancy_enabled enable_alerting = var.enable_alerting + slack_webhook_url = data.azurerm_key_vault_secret.slack_webhook_url.value } module "container-apps" { @@ -69,4 +70,5 @@ module "container-apps" { use_apex_domain = var.use_apex_domain container_memory = var.container_memory min_replicas = var.min_replicas + slack_webhook_url = data.azurerm_key_vault_secret.slack_webhook_url.value } diff --git a/lung_cancer_screening/questions/management/commands/request_summary.py b/lung_cancer_screening/questions/management/commands/request_summary.py index 6d22a67b..1b43e59b 100644 --- a/lung_cancer_screening/questions/management/commands/request_summary.py +++ b/lung_cancer_screening/questions/management/commands/request_summary.py @@ -1,5 +1,8 @@ +import json import logging +import os +import requests from django.core.management.base import BaseCommand, CommandError from lung_cancer_screening.questions.services.request_summary import RequestSummary @@ -9,13 +12,33 @@ class Command(BaseCommand): help = "Counts the number of submitted requests." def handle(self, *args, **options): - logger.info("Command: SubmittedCount.") + + logger.info("Command: Request Summary.") try: rs = RequestSummary() summary = rs.get_summary() self.stdout.write(str(summary)) + slack_webhook_url = os.environ.get("SLACK_WEBHOOK_URL") + + if not slack_webhook_url: + logger.warning("SLACK_WEBHOOK_URL is not set; skipping Slack notification.") + return + + payload = { + "text": f"*Request summary*\n```{json.dumps(summary, indent=2, default=str)}```" + } + + response = requests.post( + slack_webhook_url, + json=payload, + timeout=10, + ) + response.raise_for_status() + + logger.info("Request summary sent to Slack.") + except Exception as e: logger.error(e, exc_info=True) raise CommandError(e) From dbc5f5c3b874d4d29ffe66a36051bb190bdb7090 Mon Sep 17 00:00:00 2001 From: Alastair Lock Date: Wed, 29 Apr 2026 15:18:31 +0100 Subject: [PATCH 4/4] reformatted --- core | 0 infrastructure/modules/container-apps/jobs.tf | 1 - infrastructure/modules/infra/logic_app.tf | 10 ++++--- .../management/commands/request_summary.py | 26 ++++++++++++++++--- 4 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 core diff --git a/core b/core new file mode 100644 index 00000000..e69de29b diff --git a/infrastructure/modules/container-apps/jobs.tf b/infrastructure/modules/container-apps/jobs.tf index 7681025f..c7df424c 100644 --- a/infrastructure/modules/container-apps/jobs.tf +++ b/infrastructure/modules/container-apps/jobs.tf @@ -73,7 +73,6 @@ module "scheduled_jobs" { var.deploy_database_as_container ? local.container_db_env : local.azure_db_env ) secret_variables = merge( - # { APPLICATIONINSIGHTS_CONNECTION_STRING = var.app_insights_connection_string }, { SLACK_WEBHOOK_URL = var.slack_webhook_url }, var.deploy_database_as_container ? { DATABASE_PASSWORD = resource.random_password.admin_password[0].result } : {} ) diff --git a/infrastructure/modules/infra/logic_app.tf b/infrastructure/modules/infra/logic_app.tf index 64566b3a..960c0774 100644 --- a/infrastructure/modules/infra/logic_app.tf +++ b/infrastructure/modules/infra/logic_app.tf @@ -1,6 +1,6 @@ module "logic_app_slack_alert" { - # TODO: Add this back in before merging into main - # count = var.enable_alerting ? 1 : 0 + count = var.enable_alerting ? 1 : 0 + source = "../dtos-devops-templates/infrastructure/modules/logic-app-slack-alert" name = "logic-${var.app_short_name}-${var.environment}-slack-alerts" @@ -10,13 +10,15 @@ module "logic_app_slack_alert" { } resource "azurerm_monitor_action_group" "slack" { - name = "ag-slack-myapp-dev-uks" + count = var.enable_alerting ? 1 : 0 + + name = "ag-slack-${var.app_short_name}-${var.environment}-uks" resource_group_name = azurerm_resource_group.main.name short_name = "slack" webhook_receiver { name = "logic-app-slack" - service_uri = module.logic_app_slack_alert.trigger_callback_url + service_uri = module.logic_app_slack_alert[0].trigger_callback_url use_common_alert_schema = true } } diff --git a/lung_cancer_screening/questions/management/commands/request_summary.py b/lung_cancer_screening/questions/management/commands/request_summary.py index 1b43e59b..1ae1f511 100644 --- a/lung_cancer_screening/questions/management/commands/request_summary.py +++ b/lung_cancer_screening/questions/management/commands/request_summary.py @@ -1,4 +1,3 @@ -import json import logging import os @@ -27,8 +26,29 @@ def handle(self, *args, **options): return payload = { - "text": f"*Request summary*\n```{json.dumps(summary, indent=2, default=str)}```" - } + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "Request Summary", + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": f"*Requests:*\n{rs.get_count()}" + }, + { + "type": "mrkdwn", + "text": f"*Submitted:*\n{rs.get_submitted_count()}" + } + ] + } + ] + } response = requests.post( slack_webhook_url,