From 645c7e4f8735a706eb23b9b5230b23e2d71f48f0 Mon Sep 17 00:00:00 2001 From: John Collinson <13622412+johncollinson2001@users.noreply.github.com> Date: Mon, 30 Mar 2026 14:08:29 +0100 Subject: [PATCH 1/6] patch: Swap Trivy for Grype in CI pipeline --- .github/workflows/ci-pipeline.yaml | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci-pipeline.yaml b/.github/workflows/ci-pipeline.yaml index 2736c96..7b50c2a 100644 --- a/.github/workflows/ci-pipeline.yaml +++ b/.github/workflows/ci-pipeline.yaml @@ -12,6 +12,15 @@ on: - main pull_request: workflow_dispatch: + inputs: + grype_version: + description: "Grype version to install" + default: "v0.110.0" + type: string + grype_commit_sha: + description: "Grype commit SHA for install script" + default: "dee8de483dfba5b4e0bc0aa8e4ab2ce52137e490" + type: string jobs: build-verification: @@ -27,7 +36,7 @@ jobs: with: terraform_version: 1.9.3 terraform_wrapper: false - + - name: Install Go uses: actions/setup-go@v6 with: @@ -97,17 +106,12 @@ jobs: - name: Run GitLeaks Scan run: gitleaks detect --source . --config .gitleaks.toml - - name: Install Trivy + - name: Install Grype run: | - sudo apt-get update - sudo apt-get install -y wget apt-transport-https gnupg lsb-release - wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add - - echo deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main | sudo tee -a /etc/apt/sources.list.d/trivy.list - sudo apt-get update - sudo apt-get install -y trivy + curl -sSfL https://raw.githubusercontent.com/anchore/grype/${{ inputs.grype_commit_sha || 'dee8de483dfba5b4e0bc0aa8e4ab2ce52137e490' }}/install.sh | bash -s -- -b /usr/local/bin ${{ inputs.grype_version || 'v0.110.0' }} - - name: Run Trivy Scan - run: trivy filesystem --security-checks vuln,config --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed . + - name: Run Grype Filesystem Scan + run: grype "dir:./" --fail-on high publishing: name: Publish Release @@ -118,7 +122,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: - persist-credentials: false # Disable default GITHUB_TOKEN persistence + persist-credentials: false # Disable default GITHUB_TOKEN persistence - name: Create Semantic Release uses: cycjimmy/semantic-release-action@v4.1.0 @@ -132,4 +136,4 @@ jobs: echo ${{ steps.semantic-release.outputs.new_release_version }} echo ${{ steps.semantic-release.outputs.new_release_major_version }} echo ${{ steps.semantic-release.outputs.new_release_minor_version }} - echo ${{ steps.semantic-release.outputs.new_release_patch_version }} \ No newline at end of file + echo ${{ steps.semantic-release.outputs.new_release_patch_version }} From 7262da02bed3d8e67ee066e3068ebef2fdfdefc0 Mon Sep 17 00:00:00 2001 From: John Collinson <13622412+johncollinson2001@users.noreply.github.com> Date: Tue, 31 Mar 2026 12:16:39 +0100 Subject: [PATCH 2/6] test: Update retention periods and backup intervals for blob storage, managed disks, and PostgreSQL flexible servers --- tests/end-to-end-tests/blob_storage_backup_test.go | 6 +++--- tests/end-to-end-tests/managed_disk_backup_test.go | 6 +++--- .../postgresql_flexible_server_backup_test.go | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/end-to-end-tests/blob_storage_backup_test.go b/tests/end-to-end-tests/blob_storage_backup_test.go index c56d657..6dedce6 100644 --- a/tests/end-to-end-tests/blob_storage_backup_test.go +++ b/tests/end-to-end-tests/blob_storage_backup_test.go @@ -77,15 +77,15 @@ func TestBlobStorageBackup(t *testing.T) { blobStorageBackups := map[string]map[string]interface{}{ "backup1": { "backup_name": "blob1", - "retention_period": "P1D", - "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P1D"}, + "retention_period": "P6D", + "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P6D"}, "storage_account_id": *externalResources.StorageAccountOne.ID, "storage_account_containers": []string{*externalResources.StorageAccountOneContainer.Name}, }, "backup2": { "backup_name": "blob2", "retention_period": "P7D", - "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P2D"}, + "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P7D"}, "storage_account_id": *externalResources.StorageAccountTwo.ID, "storage_account_containers": []string{*externalResources.StorageAccountTwoContainer.Name}, }, diff --git a/tests/end-to-end-tests/managed_disk_backup_test.go b/tests/end-to-end-tests/managed_disk_backup_test.go index 17bcf82..7e46850 100644 --- a/tests/end-to-end-tests/managed_disk_backup_test.go +++ b/tests/end-to-end-tests/managed_disk_backup_test.go @@ -71,8 +71,8 @@ func TestManagedDiskBackup(t *testing.T) { managedDiskBackups := map[string]map[string]interface{}{ "backup1": { "backup_name": "disk1", - "retention_period": "P1D", - "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P1D"}, + "retention_period": "P6D", + "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P6D"}, "managed_disk_id": *externalResources.ManagedDiskOne.ID, "managed_disk_resource_group": map[string]interface{}{ "id": *externalResources.ResourceGroup.ID, @@ -82,7 +82,7 @@ func TestManagedDiskBackup(t *testing.T) { "backup2": { "backup_name": "disk2", "retention_period": "P7D", - "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P2D"}, + "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P7D"}, "managed_disk_id": *externalResources.ManagedDiskTwo.ID, "managed_disk_resource_group": map[string]interface{}{ "id": *externalResources.ResourceGroup.ID, diff --git a/tests/end-to-end-tests/postgresql_flexible_server_backup_test.go b/tests/end-to-end-tests/postgresql_flexible_server_backup_test.go index d57c1a8..227cb0f 100644 --- a/tests/end-to-end-tests/postgresql_flexible_server_backup_test.go +++ b/tests/end-to-end-tests/postgresql_flexible_server_backup_test.go @@ -71,15 +71,15 @@ func TestPostgresqlFlexibleServerBackup(t *testing.T) { PostgresqlFlexibleServerBackups := map[string]map[string]interface{}{ "backup1": { "backup_name": "server1", - "retention_period": "P1D", - "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P1D"}, + "retention_period": "P6D", + "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P6D"}, "server_id": *externalResources.PostgresqlFlexibleServerOne.ID, "server_resource_group_id": *externalResources.ResourceGroup.ID, }, "backup2": { "backup_name": "server2", "retention_period": "P7D", - "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P2D"}, + "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P7D"}, "server_id": *externalResources.PostgresqlFlexibleServerTwo.ID, "server_resource_group_id": *externalResources.ResourceGroup.ID, }, From 2a9b4a90dd6a7640bff873fea9d603fed2d396a9 Mon Sep 17 00:00:00 2001 From: John Collinson <13622412+johncollinson2001@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:17:44 +0100 Subject: [PATCH 3/6] test: Update backup intervals for blob storage, managed disks, and PostgreSQL flexible servers --- tests/end-to-end-tests/blob_storage_backup_test.go | 4 ++-- tests/end-to-end-tests/managed_disk_backup_test.go | 4 ++-- .../postgresql_flexible_server_backup_test.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/end-to-end-tests/blob_storage_backup_test.go b/tests/end-to-end-tests/blob_storage_backup_test.go index 6dedce6..7d523f0 100644 --- a/tests/end-to-end-tests/blob_storage_backup_test.go +++ b/tests/end-to-end-tests/blob_storage_backup_test.go @@ -78,14 +78,14 @@ func TestBlobStorageBackup(t *testing.T) { "backup1": { "backup_name": "blob1", "retention_period": "P6D", - "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P6D"}, + "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P1D"}, "storage_account_id": *externalResources.StorageAccountOne.ID, "storage_account_containers": []string{*externalResources.StorageAccountOneContainer.Name}, }, "backup2": { "backup_name": "blob2", "retention_period": "P7D", - "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P7D"}, + "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P1W"}, "storage_account_id": *externalResources.StorageAccountTwo.ID, "storage_account_containers": []string{*externalResources.StorageAccountTwoContainer.Name}, }, diff --git a/tests/end-to-end-tests/managed_disk_backup_test.go b/tests/end-to-end-tests/managed_disk_backup_test.go index 7e46850..2d0a6aa 100644 --- a/tests/end-to-end-tests/managed_disk_backup_test.go +++ b/tests/end-to-end-tests/managed_disk_backup_test.go @@ -72,7 +72,7 @@ func TestManagedDiskBackup(t *testing.T) { "backup1": { "backup_name": "disk1", "retention_period": "P6D", - "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P6D"}, + "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/PT6H"}, "managed_disk_id": *externalResources.ManagedDiskOne.ID, "managed_disk_resource_group": map[string]interface{}{ "id": *externalResources.ResourceGroup.ID, @@ -82,7 +82,7 @@ func TestManagedDiskBackup(t *testing.T) { "backup2": { "backup_name": "disk2", "retention_period": "P7D", - "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P7D"}, + "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P1D"}, "managed_disk_id": *externalResources.ManagedDiskTwo.ID, "managed_disk_resource_group": map[string]interface{}{ "id": *externalResources.ResourceGroup.ID, diff --git a/tests/end-to-end-tests/postgresql_flexible_server_backup_test.go b/tests/end-to-end-tests/postgresql_flexible_server_backup_test.go index 227cb0f..e9881ad 100644 --- a/tests/end-to-end-tests/postgresql_flexible_server_backup_test.go +++ b/tests/end-to-end-tests/postgresql_flexible_server_backup_test.go @@ -72,14 +72,14 @@ func TestPostgresqlFlexibleServerBackup(t *testing.T) { "backup1": { "backup_name": "server1", "retention_period": "P6D", - "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P6D"}, + "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P1W"}, "server_id": *externalResources.PostgresqlFlexibleServerOne.ID, "server_resource_group_id": *externalResources.ResourceGroup.ID, }, "backup2": { "backup_name": "server2", "retention_period": "P7D", - "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P7D"}, + "backup_intervals": []string{"R/2024-01-01T00:00:00+00:00/P1W"}, "server_id": *externalResources.PostgresqlFlexibleServerTwo.ID, "server_resource_group_id": *externalResources.ResourceGroup.ID, }, From b1c2dc26f803ed7b51ab28e208b67fee019cd9ec Mon Sep 17 00:00:00 2001 From: John Collinson <13622412+johncollinson2001@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:56:18 +0100 Subject: [PATCH 4/6] fix: Update backup intervals for blob storage, managed disks, and PostgreSQL flexible servers to align with new frequency requirements --- docs/usage.md | 14 +++---- infrastructure/variables.tf | 32 +++++++++++++++ .../backup_modules_blob_storage.tftest.hcl | 33 ++++++++++++++- .../backup_modules_managed_disk.tftest.hcl | 36 +++++++++++++++- ...ules_postgresql_flexible_server.tftest.hcl | 41 ++++++++++++++++--- 5 files changed, 139 insertions(+), 17 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index c05fad3..90bfdfe 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -79,7 +79,7 @@ module "my_backup" { backup2 = { backup_name = "storage2" retention_period = "P30D" - backup_intervals = ["R/2024-01-01T00:00:00+00:00/P2D"] + backup_intervals = ["R/2024-01-01T00:00:00+00:00/P1W"] storage_account_id = azurerm_storage_account.my_storage_account_2.id storage_account_containers = ["container1", "container2"] backup_policy_naming_template = "nhsuk-{resource_abbreviation}-{resource_type}-{backup_name}" @@ -100,7 +100,7 @@ module "my_backup" { backup2 = { backup_name = "disk2" retention_period = "P30D" - backup_intervals = ["R/2024-01-01T00:00:00+00:00/P2D"] + backup_intervals = ["R/2024-01-01T00:00:00+00:00/PT12H"] managed_disk_id = azurerm_managed_disk.my_managed_disk_2.id backup_policy_naming_template = "nhsuk-{resource_abbreviation}-{resource_type}-{backup_name}" backup_instance_naming_template = "nhsuk-{resource_abbreviation}-{resource_type}-{backup_name}" @@ -114,14 +114,14 @@ module "my_backup" { backup1 = { backup_name = "server1" retention_period = "P7D" - backup_intervals = ["R/2024-01-01T00:00:00+00:00/P1D"] + backup_intervals = ["R/2024-01-01T00:00:00+00:00/P1W"] server_id = azurerm_postgresql_flexible_server.my_server_1.id server_resource_group_id = azurerm_resource_group.my_resource_group.id } backup2 = { backup_name = "server2" retention_period = "P30D" - backup_intervals = ["R/2024-01-01T00:00:00+00:00/P2D"] + backup_intervals = ["R/2024-01-01T00:00:00+00:00/P1W"] server_id = azurerm_postgresql_flexible_server.my_server_2.id server_resource_group_id = azurerm_resource_group.my_resource_group.id backup_policy_naming_template = "nhsuk-{resource_abbreviation}-{resource_type}-{backup_name}" @@ -150,7 +150,7 @@ module "my_backup" { | `blob_storage_backups.storage_account_containers` | A list of containers in the storage account that should be backed up. | Yes | n/a | | `blob_storage_backups.backup_name` | The name of the backup, which must be unique across blob storage backups. | Yes | n/a | | `blob_storage_backups.retention_period` | How long the backed up data will be retained for, which should be in `ISO 8601` duration format. This must be specified in days, and can be up to 7 days unless `use_extended_retention` is on. [See the following link for more information about the format](https://en.wikipedia.org/wiki/ISO_8601#Durations). | Yes | n/a | -| `blob_storage_backups.backup_intervals` | A list of intervals at which backups should be taken, which should be in `ISO 8601` duration format. [See the following link for the possible values](https://en.wikipedia.org/wiki/ISO_8601#Time_intervals). | Yes | n/a | +| `blob_storage_backups.backup_intervals` | A list of intervals at which backups should be taken, in `ISO 8601` repeating interval format. The frequency (duration) part must be `P1D` (daily) or `P1W` (weekly). [See the Azure Blob backup documentation for supported schedules](https://learn.microsoft.com/en-us/azure/backup/blob-backup-configure-manage). | Yes | n/a | | `blob_storage_backups.backup_policy_naming_template` | Naming template used to construct the blob backup instance name. The following placeholders are supported and will be replaced by the module: `{resource_abbreviation}` → `bkpol`, `{resource_type}` → `blob`, `{backup_name}` → value of `blob_storage_backups.backup_name` | No | {resource_abbreviation}-{resource_type}-{backup_name} | | `blob_storage_backups.backup_instance_naming_template` | Naming template used to construct the blob backup instance name. The following placeholders are supported and will be replaced by the module: `{resource_abbreviation}` → `bkinst`, `{resource_type}` → `blob`, `{backup_name}` → value of `blob_storage_backups.backup_name` | No | {resource_abbreviation}-{resource_type}-{backup_name} | | `blob_storage_backups.time_zone` | The time zone to apply to the backup policy schedule (eg. Europe/London). If not specified, Azure’s default time zone behaviour is used. | No | n/a | @@ -159,7 +159,7 @@ module "my_backup" { | `managed_disk_backups.managed_disk_id` | The id of the managed disk that should be backed up. | Yes | n/a | | `managed_disk_backups.backup_name` | The name of the backup, which must be unique across managed disk backups. | Yes | n/a | | `managed_disk_backups.retention_period` | How long the backed up data will be retained for, which should be in `ISO 8601` duration format. This must be specified in days, and can be up to 7 days unless `use_extended_retention` is on. [See the following link for more information about the format](https://en.wikipedia.org/wiki/ISO_8601#Durations). | Yes | n/a | -| `managed_disk_backups.backup_intervals` | A list of intervals at which backups should be taken, which should be in `ISO 8601` duration format. [See the following link for the possible values](https://en.wikipedia.org/wiki/ISO_8601#Time_intervals). | Yes | n/a | +| `managed_disk_backups.backup_intervals` | A list of intervals at which backups should be taken, in `ISO 8601` repeating interval format. The frequency (duration) part must be one of `PT1H`, `PT2H`, `PT4H`, `PT6H`, `PT8H`, `PT12H` (hourly) or `P1D` (daily). [See the Azure Disk backup support matrix for supported schedules](https://learn.microsoft.com/en-us/azure/backup/disk-backup-support-matrix). | Yes | n/a | | `managed_disk_backup.backup_policy_naming_template` | Naming template used to construct the disk backup instance name. The following placeholders are supported and will be replaced by the module: `{resource_abbreviation}` → `bkpol`, `{resource_type}` → `disk`, `{backup_name}` → value of `managed_disk_backup.backup_name` | No | {resource_abbreviation}-{resource_type}-{backup_name} | | `managed_disk_backup.backup_instance_naming_template` | Naming template used to construct the disk backup instance name. The following placeholders are supported and will be replaced by the module: `{resource_abbreviation}` → `bkinst`, `{resource_type}` → `disk`, `{backup_name}` → value of `managed_disk_backup.backup_name` | No | {resource_abbreviation}-{resource_type}-{backup_name} | | `postgresql_flexible_server_backups` | A map of postgresql flexible server backups that should be created. For each backup the following values should be provided: `backup_name`, `server_id`, `server_resource_group_id`, `retention_period` and `backup_intervals`. When no value is provided then no backups are created. | No | n/a | @@ -167,6 +167,6 @@ module "my_backup" { | `postgresql_flexible_server_backups.server_id` | The id of the postgresql flexible server that should be backed up. | Yes | n/a | | `postgresql_flexible_server_backups.server_resource_group_id` | The id of the resource group which the postgresql flexible server resides in. | Yes | n/a | | `postgresql_flexible_server_backups.retention_period` | How long the backed up data will be retained for, which should be in `ISO 8601` duration format. This must be specified in days, and can be up to 7 days unless `use_extended_retention` is on. [See the following link for more information about the format](https://en.wikipedia.org/wiki/ISO_8601#Durations). | Yes | n/a | -| `postgresql_flexible_server_backups.backup_intervals` | A list of intervals at which backups should be taken, which should be in `ISO 8601` duration format. [See the following link for the possible values](https://en.wikipedia.org/wiki/ISO_8601#Time_intervals). | Yes | n/a | +| `postgresql_flexible_server_backups.backup_intervals` | A list of intervals at which backups should be taken, in `ISO 8601` repeating interval format. Only `P1W` (weekly) is supported. [See the Azure PostgreSQL Flexible Server backup support matrix for supported schedules](https://learn.microsoft.com/en-us/azure/backup/backup-azure-database-postgresql-flex-support-matrix). | Yes | n/a | | `postgresql_flexible_server_backup.backup_policy_naming_template` | Naming template used to construct the pgflex server backup instance name. The following placeholders are supported and will be replaced by the module: `{resource_abbreviation}` → `bkpol`, `{resource_type}` → `pgflex`, `{backup_name}` → value of `postgresql_flexible_server_backup.backup_name` | No | {resource_abbreviation}-{resource_type}-{backup_name} | | `postgresql_flexible_server_backup.backup_instance_naming_template` | Naming template used to construct the pgflex server backup instance name. The following placeholders are supported and will be replaced by the module: `{resource_abbreviation}` → `bkinst`, `{resource_type}` → `pgflex`, `{backup_name}` → value of `postgresql_flexible_server_backup.backup_name` | No | {resource_abbreviation}-{resource_type}-{backup_name} | diff --git a/infrastructure/variables.tf b/infrastructure/variables.tf index 1fbe598..49510c1 100644 --- a/infrastructure/variables.tf +++ b/infrastructure/variables.tf @@ -1,6 +1,11 @@ locals { # The valid backup retention period - up to 7 days, which can be bypassed when use_extended_retention is set to true valid_retention_periods = [for days in range(1, 8) : "P${days}D"] + + # Valid backup interval suffixes per resource type (the duration part after the last '/' in the repeating interval) + valid_blob_storage_intervals = ["P1D", "P1W"] + valid_managed_disk_intervals = ["PT1H", "PT2H", "PT4H", "PT6H", "PT8H", "PT12H", "P1D"] + valid_postgresql_flexible_server_intervals = ["P1W"] } variable "resource_group_name" { @@ -75,6 +80,15 @@ variable "blob_storage_backups" { error_message = "At least one backup interval must be provided." } + validation { + condition = alltrue([ + for k, v in var.blob_storage_backups : alltrue([ + for interval in v.backup_intervals : contains(local.valid_blob_storage_intervals, element(split("/", interval), length(split("/", interval)) - 1)) + ]) + ]) + error_message = "Invalid backup interval for blob storage: allowed frequencies are P1D (daily) or P1W (weekly). See https://learn.microsoft.com/en-us/azure/backup/blob-backup-configure-manage for details." + } + validation { condition = alltrue([for k, v in var.blob_storage_backups : length(v.storage_account_containers) > 0]) error_message = "At least one storage account container must be provided." @@ -108,6 +122,15 @@ variable "managed_disk_backups" { error_message = "At least one backup interval must be provided." } + validation { + condition = alltrue([ + for k, v in var.managed_disk_backups : alltrue([ + for interval in v.backup_intervals : contains(local.valid_managed_disk_intervals, element(split("/", interval), length(split("/", interval)) - 1)) + ]) + ]) + error_message = "Invalid backup interval for managed disk: allowed frequencies are PT1H, PT2H, PT4H, PT6H, PT8H, PT12H (hourly) or P1D (daily). See https://learn.microsoft.com/en-us/azure/backup/disk-backup-support-matrix for details." + } + validation { condition = var.use_extended_retention ? true : alltrue([for k, v in var.managed_disk_backups : contains(local.valid_retention_periods, v.retention_period)]) error_message = "Invalid retention period: valid periods are up to 7 days. If you require a longer retention period then please set use_extended_retention to true." @@ -133,6 +156,15 @@ variable "postgresql_flexible_server_backups" { error_message = "At least one backup interval must be provided." } + validation { + condition = alltrue([ + for k, v in var.postgresql_flexible_server_backups : alltrue([ + for interval in v.backup_intervals : contains(local.valid_postgresql_flexible_server_intervals, element(split("/", interval), length(split("/", interval)) - 1)) + ]) + ]) + error_message = "Invalid backup interval for PostgreSQL flexible server: only P1W (weekly) is allowed. See https://learn.microsoft.com/en-us/azure/backup/backup-azure-database-postgresql-flex-support-matrix for details." + } + validation { condition = var.use_extended_retention ? true : alltrue([for k, v in var.postgresql_flexible_server_backups : contains(local.valid_retention_periods, v.retention_period)]) error_message = "Invalid retention period: valid periods are up to 7 days. If you require a longer retention period then please set use_extended_retention to true." diff --git a/tests/integration-tests/backup_modules_blob_storage.tftest.hcl b/tests/integration-tests/backup_modules_blob_storage.tftest.hcl index 0344149..7464bf6 100644 --- a/tests/integration-tests/backup_modules_blob_storage.tftest.hcl +++ b/tests/integration-tests/backup_modules_blob_storage.tftest.hcl @@ -32,7 +32,7 @@ run "create_blob_storage_backup" { backup2 = { backup_name = "storage2" retention_period = "P7D" - backup_intervals = ["R/2024-01-01T00:00:00+00:00/P2D"] + backup_intervals = ["R/2024-01-01T00:00:00+00:00/P1W"] storage_account_id = "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.Storage/storageAccounts/sastorage2" storage_account_containers = ["container2"] } @@ -125,7 +125,7 @@ run "create_blob_storage_backup" { } assert { - condition = module.blob_storage_backup["backup2"].backup_policy.backup_repeating_time_intervals[0] == "R/2024-01-01T00:00:00+00:00/P2D" + condition = module.blob_storage_backup["backup2"].backup_policy.backup_repeating_time_intervals[0] == "R/2024-01-01T00:00:00+00:00/P1W" error_message = "Blob storage backup policy backup intervals not as expected." } @@ -254,6 +254,35 @@ run "validate_backup_intervals" { ] } +run "validate_backup_intervals_invalid_frequency" { + command = plan + + module { + source = "../../infrastructure" + } + + variables { + resource_group_name = run.setup_tests.resource_group_name + resource_group_location = "uksouth" + backup_vault_name = run.setup_tests.backup_vault_name + log_analytics_workspace_id = run.setup_tests.log_analytics_workspace_id + tags = run.setup_tests.tags + blob_storage_backups = { + backup1 = { + backup_name = "storage1" + retention_period = "P7D" + backup_intervals = ["R/2024-01-01T00:00:00+00:00/P2D"] + storage_account_id = "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.Storage/storageAccounts/sastorage1" + storage_account_containers = ["container1"] + } + } + } + + expect_failures = [ + var.blob_storage_backups, + ] +} + run "validate_storage_account_containers" { command = plan diff --git a/tests/integration-tests/backup_modules_managed_disk.tftest.hcl b/tests/integration-tests/backup_modules_managed_disk.tftest.hcl index e6cee41..51a9d57 100644 --- a/tests/integration-tests/backup_modules_managed_disk.tftest.hcl +++ b/tests/integration-tests/backup_modules_managed_disk.tftest.hcl @@ -35,7 +35,7 @@ run "create_managed_disk_backup" { backup2 = { backup_name = "disk2" retention_period = "P7D" - backup_intervals = ["R/2024-01-01T00:00:00+00:00/P2D"] + backup_intervals = ["R/2024-01-01T00:00:00+00:00/PT12H"] managed_disk_id = "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.Compute/disks/disk-2" managed_disk_resource_group = { id = "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group2" @@ -131,7 +131,7 @@ run "create_managed_disk_backup" { } assert { - condition = module.managed_disk_backup["backup2"].backup_policy.backup_repeating_time_intervals[0] == "R/2024-01-01T00:00:00+00:00/P2D" + condition = module.managed_disk_backup["backup2"].backup_policy.backup_repeating_time_intervals[0] == "R/2024-01-01T00:00:00+00:00/PT12H" error_message = "Managed disk backup policy backup intervals not as expected." } @@ -268,3 +268,35 @@ run "validate_backup_intervals" { var.managed_disk_backups, ] } + +run "validate_backup_intervals_invalid_frequency" { + command = plan + + module { + source = "../../infrastructure" + } + + variables { + resource_group_name = run.setup_tests.resource_group_name + resource_group_location = "uksouth" + backup_vault_name = run.setup_tests.backup_vault_name + log_analytics_workspace_id = run.setup_tests.log_analytics_workspace_id + tags = run.setup_tests.tags + managed_disk_backups = { + backup1 = { + backup_name = "disk1" + retention_period = "P7D" + backup_intervals = ["R/2024-01-01T00:00:00+00:00/P2D"] + managed_disk_id = "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.Compute/disks/disk-1" + managed_disk_resource_group = { + id = "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group1" + name = "example-resource-group1" + } + } + } + } + + expect_failures = [ + var.managed_disk_backups, + ] +} diff --git a/tests/integration-tests/backup_modules_postgresql_flexible_server.tftest.hcl b/tests/integration-tests/backup_modules_postgresql_flexible_server.tftest.hcl index c33bd39..cfb03a4 100644 --- a/tests/integration-tests/backup_modules_postgresql_flexible_server.tftest.hcl +++ b/tests/integration-tests/backup_modules_postgresql_flexible_server.tftest.hcl @@ -25,14 +25,14 @@ run "create_postgresql_flexible_server_backup" { backup1 = { backup_name = "server1" retention_period = "P1D" - backup_intervals = ["R/2024-01-01T00:00:00+00:00/P1D"] + backup_intervals = ["R/2024-01-01T00:00:00+00:00/P1W"] server_id = "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.DBforPostgreSQL/flexibleServers/server-1" server_resource_group_id = "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group1" } backup2 = { backup_name = "server2" retention_period = "P7D" - backup_intervals = ["R/2024-01-01T00:00:00+00:00/P2D"] + backup_intervals = ["R/2024-01-01T00:00:00+00:00/P1W"] server_id = "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.DBforPostgreSQL/flexibleServers/server-2" server_resource_group_id = "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group2" } @@ -65,7 +65,7 @@ run "create_postgresql_flexible_server_backup" { } assert { - condition = module.postgresql_flexible_server_backup["backup1"].backup_policy.backup_repeating_time_intervals[0] == "R/2024-01-01T00:00:00+00:00/P1D" + condition = module.postgresql_flexible_server_backup["backup1"].backup_policy.backup_repeating_time_intervals[0] == "R/2024-01-01T00:00:00+00:00/P1W" error_message = "Postgresql flexible server backup policy backup intervals not as expected." } @@ -120,7 +120,7 @@ run "create_postgresql_flexible_server_backup" { } assert { - condition = module.postgresql_flexible_server_backup["backup2"].backup_policy.backup_repeating_time_intervals[0] == "R/2024-01-01T00:00:00+00:00/P2D" + condition = module.postgresql_flexible_server_backup["backup2"].backup_policy.backup_repeating_time_intervals[0] == "R/2024-01-01T00:00:00+00:00/P1W" error_message = "Postgresql flexible server backup policy backup intervals not as expected." } @@ -172,7 +172,7 @@ run "validate_retention_period" { backup1 = { backup_name = "server1" retention_period = "P30D" - backup_intervals = ["R/2024-01-01T00:00:00+00:00/P1D"] + backup_intervals = ["R/2024-01-01T00:00:00+00:00/P1W"] server_id = "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.DBforPostgreSQL/flexibleServers/server-1" server_resource_group_id = "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group1" } @@ -202,7 +202,7 @@ run "validate_retention_period_with_extended_retention" { backup1 = { backup_name = "server1" retention_period = "P30D" - backup_intervals = ["R/2024-01-01T00:00:00+00:00/P1D"] + backup_intervals = ["R/2024-01-01T00:00:00+00:00/P1W"] server_id = "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.DBforPostgreSQL/flexibleServers/server-1" server_resource_group_id = "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group1" } @@ -243,3 +243,32 @@ run "validate_backup_intervals" { var.postgresql_flexible_server_backups, ] } + +run "validate_backup_intervals_invalid_frequency" { + command = plan + + module { + source = "../../infrastructure" + } + + variables { + resource_group_name = run.setup_tests.resource_group_name + resource_group_location = "uksouth" + backup_vault_name = run.setup_tests.backup_vault_name + log_analytics_workspace_id = run.setup_tests.log_analytics_workspace_id + tags = run.setup_tests.tags + postgresql_flexible_server_backups = { + backup1 = { + backup_name = "server1" + retention_period = "P7D" + backup_intervals = ["R/2024-01-01T00:00:00+00:00/P1D"] + server_id = "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.DBforPostgreSQL/flexibleServers/server-1" + server_resource_group_id = "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group1" + } + } + } + + expect_failures = [ + var.postgresql_flexible_server_backups, + ] +} From a4f114dbec151820e5c667df8fff506f8a3888b4 Mon Sep 17 00:00:00 2001 From: John Collinson <13622412+johncollinson2001@users.noreply.github.com> Date: Mon, 13 Apr 2026 08:54:18 +0100 Subject: [PATCH 5/6] test: Add validation for invalid backup interval structures across blob storage, managed disks, and PostgreSQL flexible servers --- infrastructure/variables.tf | 14 +++++--- tests/integration-tests/.terraform.lock.hcl | 2 ++ .../backup_modules_blob_storage.tftest.hcl | 29 +++++++++++++++++ .../backup_modules_managed_disk.tftest.hcl | 32 +++++++++++++++++++ ...ules_postgresql_flexible_server.tftest.hcl | 29 +++++++++++++++++ 5 files changed, 102 insertions(+), 4 deletions(-) diff --git a/infrastructure/variables.tf b/infrastructure/variables.tf index 49510c1..ac11937 100644 --- a/infrastructure/variables.tf +++ b/infrastructure/variables.tf @@ -2,10 +2,16 @@ locals { # The valid backup retention period - up to 7 days, which can be bypassed when use_extended_retention is set to true valid_retention_periods = [for days in range(1, 8) : "P${days}D"] - # Valid backup interval suffixes per resource type (the duration part after the last '/' in the repeating interval) + # Valid backup interval frequencies per resource type valid_blob_storage_intervals = ["P1D", "P1W"] valid_managed_disk_intervals = ["PT1H", "PT2H", "PT4H", "PT6H", "PT8H", "PT12H", "P1D"] valid_postgresql_flexible_server_intervals = ["P1W"] + + # Repeating interval format: R// + backup_interval_timestamp_pattern = "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(Z|[+-]\\d{2}:\\d{2})" + blob_storage_interval_pattern = "^R/${local.backup_interval_timestamp_pattern}/(${join("|", local.valid_blob_storage_intervals)})$" + managed_disk_interval_pattern = "^R/${local.backup_interval_timestamp_pattern}/(${join("|", local.valid_managed_disk_intervals)})$" + postgresql_interval_pattern = "^R/${local.backup_interval_timestamp_pattern}/(${join("|", local.valid_postgresql_flexible_server_intervals)})$" } variable "resource_group_name" { @@ -83,7 +89,7 @@ variable "blob_storage_backups" { validation { condition = alltrue([ for k, v in var.blob_storage_backups : alltrue([ - for interval in v.backup_intervals : contains(local.valid_blob_storage_intervals, element(split("/", interval), length(split("/", interval)) - 1)) + for interval in v.backup_intervals : can(regex(local.blob_storage_interval_pattern, interval)) ]) ]) error_message = "Invalid backup interval for blob storage: allowed frequencies are P1D (daily) or P1W (weekly). See https://learn.microsoft.com/en-us/azure/backup/blob-backup-configure-manage for details." @@ -125,7 +131,7 @@ variable "managed_disk_backups" { validation { condition = alltrue([ for k, v in var.managed_disk_backups : alltrue([ - for interval in v.backup_intervals : contains(local.valid_managed_disk_intervals, element(split("/", interval), length(split("/", interval)) - 1)) + for interval in v.backup_intervals : can(regex(local.managed_disk_interval_pattern, interval)) ]) ]) error_message = "Invalid backup interval for managed disk: allowed frequencies are PT1H, PT2H, PT4H, PT6H, PT8H, PT12H (hourly) or P1D (daily). See https://learn.microsoft.com/en-us/azure/backup/disk-backup-support-matrix for details." @@ -159,7 +165,7 @@ variable "postgresql_flexible_server_backups" { validation { condition = alltrue([ for k, v in var.postgresql_flexible_server_backups : alltrue([ - for interval in v.backup_intervals : contains(local.valid_postgresql_flexible_server_intervals, element(split("/", interval), length(split("/", interval)) - 1)) + for interval in v.backup_intervals : can(regex(local.postgresql_interval_pattern, interval)) ]) ]) error_message = "Invalid backup interval for PostgreSQL flexible server: only P1W (weekly) is allowed. See https://learn.microsoft.com/en-us/azure/backup/backup-azure-database-postgresql-flex-support-matrix for details." diff --git a/tests/integration-tests/.terraform.lock.hcl b/tests/integration-tests/.terraform.lock.hcl index 279276e..b9b7719 100644 --- a/tests/integration-tests/.terraform.lock.hcl +++ b/tests/integration-tests/.terraform.lock.hcl @@ -6,6 +6,7 @@ provider "registry.terraform.io/hashicorp/azurerm" { constraints = ">= 4.18.0, < 5.0.0" hashes = [ "h1:/Mwep7xvjuaQvT0cSmCJRwn4bJhPQRFYN/a9Jq+nWDs=", + "h1:ct8QFejdwiSb0+Q0DyuvPdVjlSZ8lOOKz/NXuegQ/dE=", "zh:3504c2142661ecb0dae71e38b2eb8e1766f7abd08e1979b599d5c52961e84f3c", "zh:49ad233a9506ca6815b014b5c7eb0b68c152dd1deac5763e519cebadcbad5259", "zh:58daec6f8ab1ebf60755585084a3390a25fbae2f11b39ea925037b0d510faf9b", @@ -26,6 +27,7 @@ provider "registry.terraform.io/hashicorp/random" { constraints = "3.7.2" hashes = [ "h1:0hcNr59VEJbhZYwuDE/ysmyTS0evkfcLarlni+zATPM=", + "h1:Def/iHM4HihJCIxQ8AYoxtoVL5lVlYx0V7bX91pxwgM=", "zh:14829603a32e4bc4d05062f059e545a91e27ff033756b48afbae6b3c835f508f", "zh:1527fb07d9fea400d70e9e6eb4a2b918d5060d604749b6f1c361518e7da546dc", "zh:1e86bcd7ebec85ba336b423ba1db046aeaa3c0e5f921039b3f1a6fc2f978feab", diff --git a/tests/integration-tests/backup_modules_blob_storage.tftest.hcl b/tests/integration-tests/backup_modules_blob_storage.tftest.hcl index 7464bf6..b149edd 100644 --- a/tests/integration-tests/backup_modules_blob_storage.tftest.hcl +++ b/tests/integration-tests/backup_modules_blob_storage.tftest.hcl @@ -283,6 +283,35 @@ run "validate_backup_intervals_invalid_frequency" { ] } +run "validate_backup_intervals_invalid_structure" { + command = plan + + module { + source = "../../infrastructure" + } + + variables { + resource_group_name = run.setup_tests.resource_group_name + resource_group_location = "uksouth" + backup_vault_name = run.setup_tests.backup_vault_name + log_analytics_workspace_id = run.setup_tests.log_analytics_workspace_id + tags = run.setup_tests.tags + blob_storage_backups = { + backup1 = { + backup_name = "storage1" + retention_period = "P7D" + backup_intervals = ["P1D", "R/bad-date/P1D", "R/2024-01-01T00:00:00+00:00/PT10H"] + storage_account_id = "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.Storage/storageAccounts/sastorage1" + storage_account_containers = ["container1"] + } + } + } + + expect_failures = [ + var.blob_storage_backups, + ] +} + run "validate_storage_account_containers" { command = plan diff --git a/tests/integration-tests/backup_modules_managed_disk.tftest.hcl b/tests/integration-tests/backup_modules_managed_disk.tftest.hcl index 51a9d57..a18e334 100644 --- a/tests/integration-tests/backup_modules_managed_disk.tftest.hcl +++ b/tests/integration-tests/backup_modules_managed_disk.tftest.hcl @@ -300,3 +300,35 @@ run "validate_backup_intervals_invalid_frequency" { var.managed_disk_backups, ] } + +run "validate_backup_intervals_invalid_structure" { + command = plan + + module { + source = "../../infrastructure" + } + + variables { + resource_group_name = run.setup_tests.resource_group_name + resource_group_location = "uksouth" + backup_vault_name = run.setup_tests.backup_vault_name + log_analytics_workspace_id = run.setup_tests.log_analytics_workspace_id + tags = run.setup_tests.tags + managed_disk_backups = { + backup1 = { + backup_name = "disk1" + retention_period = "P7D" + backup_intervals = ["P1D", "R/bad-date/P1D", "R/2024-01-01T00:00:00+00:00/PT10H"] + managed_disk_id = "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.Compute/disks/disk-1" + managed_disk_resource_group = { + id = "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group1" + name = "example-resource-group1" + } + } + } + } + + expect_failures = [ + var.managed_disk_backups, + ] +} diff --git a/tests/integration-tests/backup_modules_postgresql_flexible_server.tftest.hcl b/tests/integration-tests/backup_modules_postgresql_flexible_server.tftest.hcl index cfb03a4..27754ac 100644 --- a/tests/integration-tests/backup_modules_postgresql_flexible_server.tftest.hcl +++ b/tests/integration-tests/backup_modules_postgresql_flexible_server.tftest.hcl @@ -272,3 +272,32 @@ run "validate_backup_intervals_invalid_frequency" { var.postgresql_flexible_server_backups, ] } + +run "validate_backup_intervals_invalid_structure" { + command = plan + + module { + source = "../../infrastructure" + } + + variables { + resource_group_name = run.setup_tests.resource_group_name + resource_group_location = "uksouth" + backup_vault_name = run.setup_tests.backup_vault_name + log_analytics_workspace_id = run.setup_tests.log_analytics_workspace_id + tags = run.setup_tests.tags + postgresql_flexible_server_backups = { + backup1 = { + backup_name = "server1" + retention_period = "P7D" + backup_intervals = ["P1D", "R/bad-date/P1D", "R/2024-01-01T00:00:00+00:00/PT10H"] + server_id = "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.DBforPostgreSQL/flexibleServers/server-1" + server_resource_group_id = "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group1" + } + } + } + + expect_failures = [ + var.postgresql_flexible_server_backups, + ] +} From 00f7e676ea3600ede1c0f20d3f6838ffbdadf25a Mon Sep 17 00:00:00 2001 From: John Collinson <13622412+johncollinson2001@users.noreply.github.com> Date: Mon, 13 Apr 2026 10:24:55 +0100 Subject: [PATCH 6/6] Update backup interval regex to revert use of \d in favour of [0-9]. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- infrastructure/variables.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/variables.tf b/infrastructure/variables.tf index ac11937..89d8de0 100644 --- a/infrastructure/variables.tf +++ b/infrastructure/variables.tf @@ -8,7 +8,7 @@ locals { valid_postgresql_flexible_server_intervals = ["P1W"] # Repeating interval format: R// - backup_interval_timestamp_pattern = "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(Z|[+-]\\d{2}:\\d{2})" + backup_interval_timestamp_pattern = "[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(Z|[+-][0-9]{2}:[0-9]{2})" blob_storage_interval_pattern = "^R/${local.backup_interval_timestamp_pattern}/(${join("|", local.valid_blob_storage_intervals)})$" managed_disk_interval_pattern = "^R/${local.backup_interval_timestamp_pattern}/(${join("|", local.valid_managed_disk_intervals)})$" postgresql_interval_pattern = "^R/${local.backup_interval_timestamp_pattern}/(${join("|", local.valid_postgresql_flexible_server_intervals)})$"