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.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.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]
+}