diff --git a/.github/workflows/generate_terraform_docs.yaml b/.github/workflows/generate_terraform_docs.yaml new file mode 100644 index 000000000..a624dcba0 --- /dev/null +++ b/.github/workflows/generate_terraform_docs.yaml @@ -0,0 +1,30 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +name: Update Terraform documentation + +# Keep the `paths` filters below and the `terraform-directory` input in sync with +# the directories where the Terraform modules live. +on: + pull_request: + paths: + - '**/terraform/**' + - '.github/workflows/generate_terraform_docs.yaml' + push: + branches: + - main + paths: + - '**/terraform/**' + - '.github/workflows/generate_terraform_docs.yaml' + +permissions: + contents: write + pull-requests: write + +jobs: + terraform-docs: + name: Update Terraform documentation + uses: canonical/operator-workflows/.github/workflows/generate_terraform_docs.yaml@030c5b20a0bb880dcf1ed326bff0fc16b6eb6ac1 # main + secrets: inherit + with: + terraform-directory: terraform/charm/haproxy,terraform/charm/haproxy_ddos_protection_configurator,terraform/charm/haproxy_spoe_auth,terraform/product diff --git a/.github/workflows/terraform-docs.yaml b/.github/workflows/terraform-docs.yaml deleted file mode 100644 index 6e872ddd2..000000000 --- a/.github/workflows/terraform-docs.yaml +++ /dev/null @@ -1,18 +0,0 @@ -name: Update Terraform documentation - -on: - pull_request: - push: - branches: - - main - -jobs: - terraform-docs: - name: Update Terraform documentation - permissions: - contents: write - pull-requests: write - uses: canonical/operator-workflows/.github/workflows/generate_terraform_docs.yaml@main - secrets: inherit - with: - terraform-directory: terraform/charm,terraform/product diff --git a/.github/workflows/terraform_modules_compliance.yaml b/.github/workflows/terraform_modules_compliance.yaml new file mode 100644 index 000000000..e9dfbedbb --- /dev/null +++ b/.github/workflows/terraform_modules_compliance.yaml @@ -0,0 +1,17 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +name: Terraform modules compliance + +on: + workflow_dispatch: + pull_request: + paths: + - '**/terraform/**' + - '.github/workflows/terraform_modules_compliance.yaml' + +jobs: + terraform-compliance: + uses: canonical/operator-workflows/.github/workflows/terraform_modules_compliance.yaml@030c5b20a0bb880dcf1ed326bff0fc16b6eb6ac1 # main + with: + terraform-directories: '["terraform/charm/haproxy", "terraform/charm/haproxy_ddos_protection_configurator", "terraform/charm/haproxy_spoe_auth", "terraform/product"]' diff --git a/.github/workflows/terraform_modules_release.yaml b/.github/workflows/terraform_modules_release.yaml new file mode 100644 index 000000000..1ef660b3a --- /dev/null +++ b/.github/workflows/terraform_modules_release.yaml @@ -0,0 +1,22 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +name: Terraform modules release +on: + workflow_dispatch: + pull_request: + paths: + - '**/terraform/**' + - '.github/workflows/terraform_modules_release.yaml' + push: + branches: + - main + paths: + - '**/terraform/**' + - '.github/workflows/terraform_modules_release.yaml' + +jobs: + terraform: + uses: canonical/operator-workflows/.github/workflows/terraform_modules_release.yaml@030c5b20a0bb880dcf1ed326bff0fc16b6eb6ac1 # main + permissions: + contents: write diff --git a/.github/workflows/test_terraform_module.yaml b/.github/workflows/test_terraform_modules.yaml similarity index 69% rename from .github/workflows/test_terraform_module.yaml rename to .github/workflows/test_terraform_modules.yaml index f24d5ff71..0fb4d8545 100644 --- a/.github/workflows/test_terraform_module.yaml +++ b/.github/workflows/test_terraform_modules.yaml @@ -7,11 +7,12 @@ on: workflow_dispatch: pull_request: paths: - - 'terraform/**' + - '**/terraform/**' + - '.github/workflows/test_terraform_modules.yaml' jobs: terraform-tests: - uses: canonical/operator-workflows/.github/workflows/terraform_modules_test.yaml@main + uses: canonical/operator-workflows/.github/workflows/terraform_modules_test.yaml@030c5b20a0bb880dcf1ed326bff0fc16b6eb6ac1 # main secrets: inherit with: lxd-controller: true diff --git a/.licenserc.yaml b/.licenserc.yaml index 91e3fc37c..afd559728 100644 --- a/.licenserc.yaml +++ b/.licenserc.yaml @@ -40,4 +40,5 @@ header: - 'docs/**' - '.lycheeignore' - 'haproxy-route-policy/policy/migrations/*.py' + - '**/MAJOR_VERSION' comment: on-failure diff --git a/docs/release-notes/artifacts/pr0674.yaml b/docs/release-notes/artifacts/pr0674.yaml new file mode 100644 index 000000000..26e9c1e22 --- /dev/null +++ b/docs/release-notes/artifacts/pr0674.yaml @@ -0,0 +1,23 @@ +version_schema: 2 + +changes: + - title: Migrated Terraform modules to the CC008 Charm Terraform Standards + author: copilot + type: minor + description: > + All Terraform modules under `terraform/` (the `haproxy`, + `haproxy-ddos-protection-configurator` and `haproxy-spoe-auth` charm + modules, and the product module) were migrated to the CC008 Charm + Terraform Standards. Charm module `base` and `constraints` variables + now default to `null` instead of a fixed value, and `app_name`, + `channel` and `model_uuid` are no longer nullable. The product module + gained a new `risk` input variable. CI now runs the CC008 compliance + checker and releases Terraform modules with `tf-X.Y.Z` tags via the + canonical/operator-workflows reusable workflows. + urls: + pr: + - "" + related_doc: + related_issue: + visibility: internal + highlight: false diff --git a/terraform/MAJOR_VERSION b/terraform/MAJOR_VERSION new file mode 100644 index 000000000..56a6051ca --- /dev/null +++ b/terraform/MAJOR_VERSION @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/terraform/charm/haproxy/main.tf b/terraform/charm/haproxy/main.tf index 391904fc1..22b464c55 100644 --- a/terraform/charm/haproxy/main.tf +++ b/terraform/charm/haproxy/main.tf @@ -4,8 +4,8 @@ resource "juju_application" "haproxy" { name = var.app_name model_uuid = var.model_uuid - units = var.machines == null ? coalesce(var.units, 1) : null - machines = var.machines + units = length(coalesce(var.machines, [])) == 0 ? coalesce(var.units, 1) : null + machines = length(coalesce(var.machines, [])) == 0 ? null : var.machines charm { name = "haproxy" diff --git a/terraform/charm/haproxy/variables.tf b/terraform/charm/haproxy/variables.tf index af27b7f3f..093ac184e 100644 --- a/terraform/charm/haproxy/variables.tf +++ b/terraform/charm/haproxy/variables.tf @@ -5,18 +5,20 @@ variable "app_name" { description = "Application name of the deployed haproxy charm." type = string default = "haproxy" + nullable = false } variable "base" { description = "Base of the haproxy charm." type = string - default = "ubuntu@24.04" + default = null } variable "channel" { description = "Channel of the haproxy charm." type = string default = "2.8/edge" + nullable = false } variable "config" { @@ -28,12 +30,19 @@ variable "config" { variable "constraints" { description = "Haproxy constraints." type = string - default = "arch=amd64" + default = null +} + +variable "machines" { + description = "Machines to deploy haproxy units to. Takes precedence over units when set." + type = set(string) + default = [] } variable "model_uuid" { description = "ID of the Juju model to deploy to." type = string + nullable = false } variable "revision" { @@ -43,19 +52,8 @@ variable "revision" { } variable "units" { - description = "Number of haproxy units. If hacluster is enabled, it is recommended to use a value > 3 to ensure a quorum." + description = "Number of haproxy units. If hacluster is enabled, it is recommended to use a value > 3 to ensure a quorum. Ignored when machines is set." type = number - default = null -} - -variable "machines" { - description = "Machines to deploy haproxy units to. Mutually exclusive with units." - type = set(string) - default = null - - validation { - condition = var.machines == null || var.units == null - error_message = "machines and units cannot both be set." - } + default = 1 } diff --git a/terraform/charm/haproxy_ddos_protection_configurator/variables.tf b/terraform/charm/haproxy_ddos_protection_configurator/variables.tf index de7fed023..c9627bd0e 100644 --- a/terraform/charm/haproxy_ddos_protection_configurator/variables.tf +++ b/terraform/charm/haproxy_ddos_protection_configurator/variables.tf @@ -5,18 +5,20 @@ variable "app_name" { description = "Application name of the deployed haproxy-ddos-protection-configurator charm." type = string default = "haproxy-ddos-protection-configurator" + nullable = false } variable "base" { description = "Base of the haproxy-ddos-protection-configurator charm." type = string - default = "ubuntu@24.04" + default = null } variable "channel" { description = "Channel of the haproxy-ddos-protection-configurator charm." type = string default = "latest/edge" + nullable = false } variable "config" { @@ -28,12 +30,13 @@ variable "config" { variable "constraints" { description = "haproxy-ddos-protection-configurator constraints." type = string - default = "arch=amd64" + default = null } variable "model_uuid" { description = "ID of the Juju model to deploy to." type = string + nullable = false } variable "revision" { diff --git a/terraform/charm/haproxy_spoe_auth/variables.tf b/terraform/charm/haproxy_spoe_auth/variables.tf index 9df1130dd..9c041f0b4 100644 --- a/terraform/charm/haproxy_spoe_auth/variables.tf +++ b/terraform/charm/haproxy_spoe_auth/variables.tf @@ -5,18 +5,20 @@ variable "app_name" { description = "Application name of the deployed haproxy-spoe-auth charm." type = string default = "haproxy-spoe-auth" + nullable = false } variable "base" { description = "Base of the haproxy-spoe-auth charm." type = string - default = "ubuntu@24.04" + default = null } variable "channel" { description = "Channel of the haproxy-spoe-auth charm." type = string default = "latest/edge" + nullable = false } variable "config" { @@ -28,12 +30,13 @@ variable "config" { variable "constraints" { description = "haproxy-spoe-auth constraints." type = string - default = "arch=amd64" + default = null } variable "model_uuid" { description = "ID of the Juju model to deploy to." type = string + nullable = false } variable "revision" { diff --git a/terraform/product/README.md b/terraform/product/README.md index 32a62780a..ebcf3adf1 100644 --- a/terraform/product/README.md +++ b/terraform/product/README.md @@ -39,14 +39,15 @@ | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [grafana\_agent](#input\_grafana\_agent) | n/a |
object({
app_name = optional(string, "grafana-agent")
channel = optional(string, "2/stable")
config = optional(map(string), {})
revision = optional(number, null)
})
| `{}` | no | -| [hacluster](#input\_hacluster) | n/a |
object({
app_name = optional(string, "hacluster")
channel = optional(string, "2.4/edge")
config = optional(map(string), {})
revision = optional(number, null)
})
| `null` | no | -| [haproxy](#input\_haproxy) | n/a |
object({
app_name = optional(string, "haproxy")
channel = optional(string, "2.8/edge")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number)
base = optional(string, "ubuntu@24.04")
units = optional(number)
machines = optional(set(string))
})
| `{}` | no | -| [haproxy\_ddos\_protection\_configurator](#input\_haproxy\_ddos\_protection\_configurator) | Configuration for haproxy-ddos-protection-configurator charm deployment. |
object({
app_name = optional(string, "haproxy-ddos-protection-configurator")
channel = optional(string, "latest/edge")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
base = optional(string, "ubuntu@24.04")
units = optional(number, 1)
})
| `{}` | no | -| [keepalived](#input\_keepalived) | n/a |
object({
app_name = optional(string, "keepalived")
channel = optional(string, "latest/edge")
config = optional(map(string), {})
revision = optional(number, null)
})
| `null` | no | +| [grafana\_agent](#input\_grafana\_agent) | n/a |
object({
app_name = optional(string, "grafana-agent")
config = optional(map(string), {})
revision = optional(number, null)
})
| `{}` | no | +| [hacluster](#input\_hacluster) | n/a |
object({
app_name = optional(string, "hacluster")
config = optional(map(string), {})
revision = optional(number, null)
})
| `null` | no | +| [haproxy](#input\_haproxy) | n/a |
object({
app_name = optional(string, "haproxy")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number)
base = optional(string, "ubuntu@24.04")
units = optional(number)
machines = optional(set(string))
})
| `{}` | no | +| [haproxy\_ddos\_protection\_configurator](#input\_haproxy\_ddos\_protection\_configurator) | Configuration for haproxy-ddos-protection-configurator charm deployment. |
object({
app_name = optional(string, "haproxy-ddos-protection-configurator")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
base = optional(string, "ubuntu@24.04")
units = optional(number, 1)
})
| `{}` | no | +| [keepalived](#input\_keepalived) | n/a |
object({
app_name = optional(string, "keepalived")
config = optional(map(string), {})
revision = optional(number, null)
})
| `null` | no | | [metadata\_version](#input\_metadata\_version) | Version string reported in the product module 'metadata' output. | `string` | `"1.0.0"` | no | | [model\_uuid](#input\_model\_uuid) | ID of the model to deploy to | `string` | `""` | no | -| [protected\_hostnames\_configuration](#input\_protected\_hostnames\_configuration) | Configuration for each protected hostname.
For each hostname, a haproxy-spoe-auth application will be deployed and integrated to haproxy.
Optionally a oauth-external-idp-integrator application can be deployed and integrated to haproxy-spoe-auth.
The hostnames to protect have to be provided through the haproxy\_route relation. |
list(object({
hostname = string
haproxy_spoe_auth = optional(object({
# The hostname will be added automatically
config = optional(map(string), {})
# A random number will be appended to each app_name
app_name = optional(string, "haproxy-spoe-auth")
channel = optional(string, "latest/stable")
constraints = optional(string, "arch=amd64")
revision = optional(number)
base = optional(string, "ubuntu@24.04")
units = optional(number, 1)
}), {})
oauth_external_idp_integrator = optional(object({
# A number will be appended to the app_name
app_name = optional(string, "oauth-external-idp-integrator")
channel = optional(string, "latest/edge")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number)
base = optional(string, "ubuntu@22.04")
units = optional(number, 1)
}), null)
}))
| n/a | yes | +| [protected\_hostnames\_configuration](#input\_protected\_hostnames\_configuration) | Configuration for each protected hostname.
For each hostname, a haproxy-spoe-auth application will be deployed and integrated to haproxy.
Optionally a oauth-external-idp-integrator application can be deployed and integrated to haproxy-spoe-auth.
The hostnames to protect have to be provided through the haproxy\_route relation. |
list(object({
hostname = string
haproxy_spoe_auth = optional(object({
# The hostname will be added automatically
config = optional(map(string), {})
# A random number will be appended to each app_name
app_name = optional(string, "haproxy-spoe-auth")
constraints = optional(string, "arch=amd64")
revision = optional(number)
base = optional(string, "ubuntu@24.04")
units = optional(number, 1)
}), {})
oauth_external_idp_integrator = optional(object({
# A number will be appended to the app_name
app_name = optional(string, "oauth-external-idp-integrator")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number)
base = optional(string, "ubuntu@22.04")
units = optional(number, 1)
}), null)
}))
| n/a | yes | +| [risk](#input\_risk) | Risk level controlling the channel risk of the charms in the solution. | `string` | `"stable"` | no | ## Outputs diff --git a/terraform/product/locals.tf b/terraform/product/locals.tf new file mode 100644 index 000000000..7886417a5 --- /dev/null +++ b/terraform/product/locals.tf @@ -0,0 +1,16 @@ +# Copyright 2025 Canonical Ltd. +# See LICENSE file for licensing details. + +locals { + use_hacluster = var.hacluster != null + use_keepalived = var.keepalived != null + + # Product owns each charm's track; `risk` selects the channel risk solution-wide. + haproxy_channel = "2.8/${var.risk}" + hacluster_channel = "2.4/${var.risk}" + keepalived_channel = "latest/${var.risk}" + grafana_agent_channel = "2/${var.risk}" + haproxy_ddos_protection_configurator_channel = "latest/${var.risk}" + haproxy_spoe_auth_channel = "latest/${var.risk}" + oauth_external_idp_integrator_channel = "latest/${var.risk}" +} diff --git a/terraform/product/main.tf b/terraform/product/main.tf index f8f4d6284..37206a12c 100644 --- a/terraform/product/main.tf +++ b/terraform/product/main.tf @@ -1,16 +1,12 @@ # Copyright 2025 Canonical Ltd. # See LICENSE file for licensing details. -locals { - use_hacluster = var.hacluster != null - use_keepalived = var.keepalived != null -} module "haproxy" { source = "../charm/haproxy" model_uuid = var.model_uuid app_name = var.haproxy.app_name - channel = var.haproxy.channel + channel = local.haproxy_channel revision = var.haproxy.revision base = var.haproxy.base units = var.haproxy.units @@ -28,7 +24,7 @@ resource "juju_application" "hacluster" { charm { name = "hacluster" revision = var.hacluster.revision - channel = var.hacluster.channel + channel = local.hacluster_channel base = var.haproxy.base } @@ -59,7 +55,7 @@ resource "juju_application" "keepalived" { charm { name = "keepalived" revision = var.keepalived.revision - channel = var.keepalived.channel + channel = local.keepalived_channel base = var.haproxy.base } @@ -89,7 +85,7 @@ resource "juju_application" "grafana_agent" { charm { name = "grafana-agent" revision = var.grafana_agent.revision - channel = var.grafana_agent.channel + channel = local.grafana_agent_channel base = var.haproxy.base } @@ -115,7 +111,7 @@ module "haproxy_ddos_protection_configurator" { model_uuid = var.model_uuid app_name = var.haproxy_ddos_protection_configurator.app_name - channel = var.haproxy_ddos_protection_configurator.channel + channel = local.haproxy_ddos_protection_configurator_channel revision = var.haproxy_ddos_protection_configurator.revision base = var.haproxy_ddos_protection_configurator.base units = var.haproxy_ddos_protection_configurator.units @@ -147,7 +143,7 @@ module "haproxy_spoe_auth" { model_uuid = var.model_uuid app_name = format("%s%s", each.value.app_name, substr(md5(each.key), 0, 8)) - channel = each.value.channel + channel = local.haproxy_spoe_auth_channel revision = each.value.revision base = each.value.base units = each.value.units @@ -184,7 +180,7 @@ resource "juju_application" "oauth_external_idp_integrator" { charm { name = "oauth-external-idp-integrator" revision = each.value.revision - channel = each.value.channel + channel = local.oauth_external_idp_integrator_channel base = each.value.base } config = each.value.config diff --git a/terraform/product/variables.tf b/terraform/product/variables.tf index 95e8e20b2..68fb5ce37 100644 --- a/terraform/product/variables.tf +++ b/terraform/product/variables.tf @@ -4,7 +4,6 @@ variable "grafana_agent" { type = object({ app_name = optional(string, "grafana-agent") - channel = optional(string, "2/stable") config = optional(map(string), {}) revision = optional(number, null) }) @@ -14,7 +13,6 @@ variable "grafana_agent" { variable "hacluster" { type = object({ app_name = optional(string, "hacluster") - channel = optional(string, "2.4/edge") config = optional(map(string), {}) revision = optional(number, null) }) @@ -24,7 +22,6 @@ variable "hacluster" { variable "haproxy" { type = object({ app_name = optional(string, "haproxy") - channel = optional(string, "2.8/edge") config = optional(map(string), {}) constraints = optional(string, "arch=amd64") revision = optional(number) @@ -43,7 +40,6 @@ variable "haproxy_ddos_protection_configurator" { description = "Configuration for haproxy-ddos-protection-configurator charm deployment." type = object({ app_name = optional(string, "haproxy-ddos-protection-configurator") - channel = optional(string, "latest/edge") config = optional(map(string), {}) constraints = optional(string, "arch=amd64") revision = optional(number, null) @@ -56,7 +52,6 @@ variable "haproxy_ddos_protection_configurator" { variable "keepalived" { type = object({ app_name = optional(string, "keepalived") - channel = optional(string, "latest/edge") config = optional(map(string), {}) revision = optional(number, null) }) @@ -93,7 +88,6 @@ EOF config = optional(map(string), {}) # A random number will be appended to each app_name app_name = optional(string, "haproxy-spoe-auth") - channel = optional(string, "latest/stable") constraints = optional(string, "arch=amd64") revision = optional(number) base = optional(string, "ubuntu@24.04") @@ -102,7 +96,6 @@ EOF oauth_external_idp_integrator = optional(object({ # A number will be appended to the app_name app_name = optional(string, "oauth-external-idp-integrator") - channel = optional(string, "latest/edge") config = optional(map(string), {}) constraints = optional(string, "arch=amd64") revision = optional(number) @@ -111,3 +104,14 @@ EOF }), null) })) } + +variable "risk" { + description = "Risk level controlling the channel risk of the charms in the solution." + type = string + default = "stable" + + validation { + condition = contains(["stable", "edge"], var.risk) + error_message = "risk must be one of: stable, edge." + } +} diff --git a/terraform/terraform.tf b/terraform/terraform.tf deleted file mode 100644 index 03a835ec1..000000000 --- a/terraform/terraform.tf +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright 2025 Canonical Ltd. -# See LICENSE file for licensing details. - -terraform { - required_providers { - juju = { - source = "juju/juju" - version = ">= 1.0" - } - } - required_version = ">= 1.6" -} diff --git a/terraform/tests/main.tftest.hcl b/terraform/tests/main.tftest.hcl index ca83a7a12..b76f488e4 100644 --- a/terraform/tests/main.tftest.hcl +++ b/terraform/tests/main.tftest.hcl @@ -51,7 +51,6 @@ run "basic_deploy" { { hostname = "two.example.com" haproxy_spoe_auth = { - channel = "latest/edge" # renovate: depName="haproxy-spoe-auth" revision = 174 } @@ -123,3 +122,18 @@ run "haproxy_deploys_on_existing_machine" { error_message = "haproxy should be deployed on the existing machine" } } + +run "invalid_risk_is_rejected" { + command = plan + + module { + source = "./product" + } + + variables { + model_uuid = run.setup_tests.model_uuid + risk = "beta" + } + + expect_failures = [var.risk] +}