From be707bafbd0a20ca2388f34f980c37021d8c05d3 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Tue, 10 Feb 2026 09:21:12 -0500 Subject: [PATCH 1/5] feat: TF tests for channel validation --- .../tests/channel_validation.tftest.hcl | 83 ++++++++++++++ terraform/cos-lite/variables.tf | 5 + .../cos/tests/channel_validation.tftest.hcl | 107 ++++++++++++++++++ terraform/cos/variables.tf | 5 + 4 files changed, 200 insertions(+) create mode 100644 terraform/cos-lite/tests/channel_validation.tftest.hcl create mode 100644 terraform/cos/tests/channel_validation.tftest.hcl diff --git a/terraform/cos-lite/tests/channel_validation.tftest.hcl b/terraform/cos-lite/tests/channel_validation.tftest.hcl new file mode 100644 index 0000000..50f2060 --- /dev/null +++ b/terraform/cos-lite/tests/channel_validation.tftest.hcl @@ -0,0 +1,83 @@ +# ---Happy path--- + +run "valid_channel_track" { + command = plan + + variables { + channel = "2/stable" + model_uuid = "00000000-0000-0000-0000-000000000000" + } +} + +run "valid_channel_stable" { + command = plan + + variables { + channel = "2/candidate" + model_uuid = "00000000-0000-0000-0000-000000000000" + } +} + +run "valid_channel_candidate" { + command = plan + + variables { + channel = "2/beta" + model_uuid = "00000000-0000-0000-0000-000000000000" + } +} + +run "valid_channel_beta" { + command = plan + + variables { + channel = "2/edge" + model_uuid = "00000000-0000-0000-0000-000000000000" + } +} + +# ---Failure path--- + +run "invalid_channel_track" { + command = plan + + variables { + channel = "1/stable" + model_uuid = "00000000-0000-0000-0000-000000000000" + } + + expect_failures = [var.channel] +} + +run "invalid_channel_track_numeric" { + command = plan + + variables { + channel = "123/risk" + model_uuid = "00000000-0000-0000-0000-000000000000" + } + + expect_failures = [var.channel] +} + +run "invalid_channel_track_string" { + command = plan + + variables { + channel = "foo/risk" + model_uuid = "00000000-0000-0000-0000-000000000000" + } + + expect_failures = [var.channel] +} + +run "invalid_channel_track_dev" { + command = plan + + variables { + channel = "dev/risk" + model_uuid = "00000000-0000-0000-0000-000000000000" + } + + expect_failures = [var.channel] +} diff --git a/terraform/cos-lite/variables.tf b/terraform/cos-lite/variables.tf index e727469..101e696 100644 --- a/terraform/cos-lite/variables.tf +++ b/terraform/cos-lite/variables.tf @@ -14,6 +14,11 @@ variable "channel" { description = "Channel that the applications are (unless overwritten by external_channels) deployed from" type = string default = "2/stable" + + validation { + condition = split("/", var.channel)[0] == split("/", "2/stable")[0] + error_message = "The track (the part before the '/') of the channel must match the default." + } } variable "model_uuid" { diff --git a/terraform/cos/tests/channel_validation.tftest.hcl b/terraform/cos/tests/channel_validation.tftest.hcl new file mode 100644 index 0000000..8f15979 --- /dev/null +++ b/terraform/cos/tests/channel_validation.tftest.hcl @@ -0,0 +1,107 @@ +# ---Happy path--- + +run "valid_channel_track" { + command = plan + + variables { + channel = "2/stable" + model_uuid = "00000000-0000-0000-0000-000000000000" + s3_endpoint = "foo" + s3_access_key = "foo" + s3_secret_key = "foo" + } +} + +run "valid_channel_stable" { + command = plan + + variables { + channel = "2/candidate" + model_uuid = "00000000-0000-0000-0000-000000000000" + s3_endpoint = "foo" + s3_access_key = "foo" + s3_secret_key = "foo" + } +} + +run "valid_channel_candidate" { + command = plan + + variables { + channel = "2/beta" + model_uuid = "00000000-0000-0000-0000-000000000000" + s3_endpoint = "foo" + s3_access_key = "foo" + s3_secret_key = "foo" + } +} + +run "valid_channel_beta" { + command = plan + + variables { + channel = "2/edge" + model_uuid = "00000000-0000-0000-0000-000000000000" + s3_endpoint = "foo" + s3_access_key = "foo" + s3_secret_key = "foo" + } +} + +# ---Failure path--- + +run "invalid_channel_track" { + command = plan + + variables { + channel = "1/stable" + model_uuid = "00000000-0000-0000-0000-000000000000" + s3_endpoint = "foo" + s3_access_key = "foo" + s3_secret_key = "foo" + } + + expect_failures = [var.channel] +} + +run "invalid_channel_track_numeric" { + command = plan + + variables { + channel = "123/risk" + model_uuid = "00000000-0000-0000-0000-000000000000" + s3_endpoint = "foo" + s3_access_key = "foo" + s3_secret_key = "foo" + } + + expect_failures = [var.channel] +} + +run "invalid_channel_track_string" { + command = plan + + variables { + channel = "foo/risk" + model_uuid = "00000000-0000-0000-0000-000000000000" + s3_endpoint = "foo" + s3_access_key = "foo" + s3_secret_key = "foo" + } + + expect_failures = [var.channel] +} + +run "invalid_channel_track_dev" { + command = plan + + variables { + channel = "dev/risk" + model_uuid = "00000000-0000-0000-0000-000000000000" + s3_endpoint = "foo" + s3_access_key = "foo" + s3_secret_key = "foo" + } + + expect_failures = [var.channel] +} diff --git a/terraform/cos/variables.tf b/terraform/cos/variables.tf index 2ab7c6e..347d83b 100644 --- a/terraform/cos/variables.tf +++ b/terraform/cos/variables.tf @@ -14,6 +14,11 @@ variable "channel" { description = "Channel that the applications are (unless overwritten by external_channels) deployed from" type = string default = "2/stable" + + validation { + condition = split("/", var.channel)[0] == split("/", "2/stable")[0] + error_message = "The track (the part before the '/') of the channel must match the default." + } } variable "model_uuid" { From 161012ec583f7a5821453718854b38300f6c8559 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Fri, 13 Feb 2026 13:45:14 -0500 Subject: [PATCH 2/5] chore: CI --- .github/workflows/terraform.yml | 12 ++++++++++++ justfile | 13 +++++++++++++ 2 files changed, 25 insertions(+) diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml index d52feee..2f45139 100644 --- a/.github/workflows/terraform.yml +++ b/.github/workflows/terraform.yml @@ -43,6 +43,18 @@ jobs: sudo snap install just --classic - name: Validate the Terraform modules run: just validate-terraform + test-unit: + name: Terraform unit tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install dependencies + run: | + sudo snap install terraform --classic + sudo snap install just --classic + - name: Unit test the Terraform modules + run: just unit test-integration-cos-lite: name: COS Lite Terraform integration uses: canonical/observability-stack/.github/workflows/_integration.yml@main diff --git a/justfile b/justfile index 066c679..59220a7 100644 --- a/justfile +++ b/justfile @@ -20,6 +20,10 @@ lint: lint-workflows lint-terraform lint-terraform-docs [group("Format")] fmt: format-terraform format-terraform-docs +# Run unit tests +[group("Unit")] +unit: (unit-test "cos") (unit-test "cos-lite") + # Lint the Github workflows [group("Lint")] lint-workflows: @@ -50,12 +54,21 @@ format-terraform-docs: terraform-docs --config .tfdocs-config.yml . # Validate the Terraform modules +[group("Static")] [working-directory("./terraform")] validate-terraform: if [ -z "${terraform}" ]; then echo "ERROR: please install terraform or opentofu"; exit 1; fi set -e; for repo in */; do (cd "$repo" && echo "Processing ${repo%/}..." && $terraform init -upgrade && $terraform validate) || exit 1; done +# Run a unit test +[group("Unit")] +[working-directory("./terraform")] +unit-test module: + if [ -z "${terraform}" ]; then echo "ERROR: please install terraform or opentofu"; exit 1; fi + $terraform -chdir={{module}} init -upgrade && $terraform -chdir={{module}} test + # Run integration tests +[group("Integration")] [working-directory("./tests/integration")] integration *args='': uv run ${uv_flags} pytest -vv --capture=no --exitfirst "${args}" From c1f9322213befdd77277e6b1d36486c3d121a87c Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Tue, 17 Feb 2026 09:27:06 -0500 Subject: [PATCH 3/5] chore: mock provider --- terraform/cos-lite/tests/channel_validation.tftest.hcl | 2 ++ terraform/cos/tests/channel_validation.tftest.hcl | 2 ++ 2 files changed, 4 insertions(+) diff --git a/terraform/cos-lite/tests/channel_validation.tftest.hcl b/terraform/cos-lite/tests/channel_validation.tftest.hcl index 50f2060..101cd04 100644 --- a/terraform/cos-lite/tests/channel_validation.tftest.hcl +++ b/terraform/cos-lite/tests/channel_validation.tftest.hcl @@ -1,3 +1,5 @@ +mock_provider "juju" {} + # ---Happy path--- run "valid_channel_track" { diff --git a/terraform/cos/tests/channel_validation.tftest.hcl b/terraform/cos/tests/channel_validation.tftest.hcl index 8f15979..cee0b7c 100644 --- a/terraform/cos/tests/channel_validation.tftest.hcl +++ b/terraform/cos/tests/channel_validation.tftest.hcl @@ -1,3 +1,5 @@ +mock_provider "juju" {} + # ---Happy path--- run "valid_channel_track" { From 40744a2f7fd113dc73386c3e5b2768b11886863d Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Tue, 17 Feb 2026 10:10:13 -0500 Subject: [PATCH 4/5] chore: CI and docs --- justfile | 2 +- terraform/cos-lite/README.md | 14 +++++++------- terraform/cos/README.md | 16 ++++++++-------- terraform/loki/README.md | 8 ++++---- terraform/mimir/README.md | 8 ++++---- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/justfile b/justfile index 59220a7..463de79 100644 --- a/justfile +++ b/justfile @@ -39,7 +39,7 @@ lint-terraform: # Lint the Terraform documentation [group("Lint")] lint-terraform-docs: - terraform-docs --config .tfdocs-config.yml . + terraform-docs --config .tfdocs-config.yml --output-check . # Format the Terraform modules [group("Format")] diff --git a/terraform/cos-lite/README.md b/terraform/cos-lite/README.md index 5fec615..4cafb3a 100644 --- a/terraform/cos-lite/README.md +++ b/terraform/cos-lite/README.md @@ -13,11 +13,11 @@ This is a Terraform module facilitating the deployment of the COS Lite solution, | Name | Source | Version | |------|--------|---------| -| [alertmanager](#module\_alertmanager) | git::https://github.com/canonical/alertmanager-k8s-operator//terraform | n/a | -| [catalogue](#module\_catalogue) | git::https://github.com/canonical/catalogue-k8s-operator//terraform | n/a | -| [grafana](#module\_grafana) | git::https://github.com/canonical/grafana-k8s-operator//terraform | n/a | -| [loki](#module\_loki) | git::https://github.com/canonical/loki-k8s-operator//terraform | n/a | -| [prometheus](#module\_prometheus) | git::https://github.com/canonical/prometheus-k8s-operator//terraform | n/a | +| [alertmanager](#module\_alertmanager) | git::https://github.com/canonical/alertmanager-k8s-operator//terraform | track/2 | +| [catalogue](#module\_catalogue) | git::https://github.com/canonical/catalogue-k8s-operator//terraform | track/2 | +| [grafana](#module\_grafana) | git::https://github.com/canonical/grafana-k8s-operator//terraform | track/2 | +| [loki](#module\_loki) | git::https://github.com/canonical/loki-k8s-operator//terraform | track/2 | +| [prometheus](#module\_prometheus) | git::https://github.com/canonical/prometheus-k8s-operator//terraform | track/2 | | [ssc](#module\_ssc) | git::https://github.com/canonical/self-signed-certificates-operator//terraform | n/a | | [traefik](#module\_traefik) | git::https://github.com/canonical/traefik-k8s-operator//terraform | n/a | @@ -27,13 +27,13 @@ This is a Terraform module facilitating the deployment of the COS Lite solution, |------|-------------|------|---------|:--------:| | [alertmanager](#input\_alertmanager) | Application configuration for Alertmanager. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application |
object({
app_name = optional(string, "alertmanager")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | | [catalogue](#input\_catalogue) | Application configuration for Catalogue. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application |
object({
app_name = optional(string, "catalogue")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | -| [channel](#input\_channel) | Channel that the applications are (unless overwritten by external\_channels) deployed from | `string` | n/a | yes | +| [channel](#input\_channel) | Channel that the applications are (unless overwritten by external\_channels) deployed from | `string` | `"2/stable"` | no | | [external\_ca\_cert\_offer\_url](#input\_external\_ca\_cert\_offer\_url) | A Juju offer URL (e.g. admin/external-ca.send-ca-cert) of a CA providing the 'certificate\_transfer' integration for applications to trust ingress via Traefik. | `string` | `null` | no | | [external\_certificates\_offer\_url](#input\_external\_certificates\_offer\_url) | A Juju offer URL (e.g. admin/external-ca.certificates) of a CA providing the 'tls\_certificates' integration for Traefik to supply it with server certificates. | `string` | `null` | no | | [grafana](#input\_grafana) | Application configuration for Grafana. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application |
object({
app_name = optional(string, "grafana")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | | [internal\_tls](#input\_internal\_tls) | Specify whether to use TLS or not for internal COS communication. By default, TLS is enabled using self-signed-certificates | `bool` | `true` | no | | [loki](#input\_loki) | Application configuration for Loki. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application |
object({
app_name = optional(string, "loki")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | -| [model\_uuid](#input\_model\_uuid) | Reference to an existing model resource or data source for the model to deploy to | `string` | n/a | yes | +| [model\_uuid](#input\_model\_uuid) | Reference to an existing model resource or data source for the model to deploy to | `string` | `"foo"` | no | | [prometheus](#input\_prometheus) | Application configuration for Prometheus. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application |
object({
app_name = optional(string, "prometheus")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | | [ssc](#input\_ssc) | Application configuration for self-signed-certificates. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application |
object({
app_name = optional(string, "ca")
channel = optional(string, "1/stable")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | | [traefik](#input\_traefik) | Application configuration for Traefik. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application |
object({
app_name = optional(string, "traefik")
channel = optional(string, "latest/stable")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | diff --git a/terraform/cos/README.md b/terraform/cos/README.md index 4a95100..a2ac0c2 100644 --- a/terraform/cos/README.md +++ b/terraform/cos/README.md @@ -16,14 +16,14 @@ This is a Terraform module facilitating the deployment of the COS solution, usin | Name | Source | Version | |------|--------|---------| -| [alertmanager](#module\_alertmanager) | git::https://github.com/canonical/alertmanager-k8s-operator//terraform | n/a | -| [catalogue](#module\_catalogue) | git::https://github.com/canonical/catalogue-k8s-operator//terraform | n/a | -| [grafana](#module\_grafana) | git::https://github.com/canonical/grafana-k8s-operator//terraform | n/a | -| [loki](#module\_loki) | git::https://github.com/canonical/observability-stack//terraform/loki | n/a | -| [mimir](#module\_mimir) | git::https://github.com/canonical/observability-stack//terraform/mimir | n/a | -| [opentelemetry\_collector](#module\_opentelemetry\_collector) | git::https://github.com/canonical/opentelemetry-collector-k8s-operator//terraform | n/a | +| [alertmanager](#module\_alertmanager) | git::https://github.com/canonical/alertmanager-k8s-operator//terraform | track/2 | +| [catalogue](#module\_catalogue) | git::https://github.com/canonical/catalogue-k8s-operator//terraform | track/2 | +| [grafana](#module\_grafana) | git::https://github.com/canonical/grafana-k8s-operator//terraform | track/2 | +| [loki](#module\_loki) | git::https://github.com/canonical/observability-stack//terraform/loki | track/2 | +| [mimir](#module\_mimir) | git::https://github.com/canonical/observability-stack//terraform/mimir | track/2 | +| [opentelemetry\_collector](#module\_opentelemetry\_collector) | git::https://github.com/canonical/opentelemetry-collector-k8s-operator//terraform | track/2 | | [ssc](#module\_ssc) | git::https://github.com/canonical/self-signed-certificates-operator//terraform | n/a | -| [tempo](#module\_tempo) | git::https://github.com/canonical/tempo-operators//terraform | n/a | +| [tempo](#module\_tempo) | git::https://github.com/canonical/tempo-operators//terraform | track/2 | | [traefik](#module\_traefik) | git::https://github.com/canonical/traefik-k8s-operator//terraform | n/a | ## Inputs @@ -33,7 +33,7 @@ This is a Terraform module facilitating the deployment of the COS solution, usin | [alertmanager](#input\_alertmanager) | Application configuration for Alertmanager. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application |
object({
app_name = optional(string, "alertmanager")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | | [anti\_affinity](#input\_anti\_affinity) | Enable anti-affinity constraints across all HA modules (Mimir, Loki, Tempo) | `bool` | `true` | no | | [catalogue](#input\_catalogue) | Application configuration for Catalogue. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application |
object({
app_name = optional(string, "catalogue")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | -| [channel](#input\_channel) | Channel that the applications are (unless overwritten by external\_channels) deployed from | `string` | n/a | yes | +| [channel](#input\_channel) | Channel that the applications are (unless overwritten by external\_channels) deployed from | `string` | `"2/stable"` | no | | [cloud](#input\_cloud) | Kubernetes cloud or environment where this COS module will be deployed (e.g self-managed, aws) | `string` | `"self-managed"` | no | | [external\_ca\_cert\_offer\_url](#input\_external\_ca\_cert\_offer\_url) | A Juju offer URL (e.g. admin/external-ca.send-ca-cert) of a CA providing the 'certificate\_transfer' integration for applications to trust ingress via Traefik. | `string` | `null` | no | | [external\_certificates\_offer\_url](#input\_external\_certificates\_offer\_url) | A Juju offer URL of a CA providing the 'tls\_certificates' integration for Traefik to supply it with server certificates | `string` | `null` | no | diff --git a/terraform/loki/README.md b/terraform/loki/README.md index c3fa27c..a739bd6 100644 --- a/terraform/loki/README.md +++ b/terraform/loki/README.md @@ -16,10 +16,10 @@ This is a Terraform module facilitating the deployment of Loki solution, using t | Name | Source | Version | |------|--------|---------| -| [loki\_backend](#module\_loki\_backend) | git::https://github.com/canonical/loki-worker-k8s-operator//terraform | n/a | -| [loki\_coordinator](#module\_loki\_coordinator) | git::https://github.com/canonical/loki-coordinator-k8s-operator//terraform | n/a | -| [loki\_read](#module\_loki\_read) | git::https://github.com/canonical/loki-worker-k8s-operator//terraform | n/a | -| [loki\_write](#module\_loki\_write) | git::https://github.com/canonical/loki-worker-k8s-operator//terraform | n/a | +| [loki\_backend](#module\_loki\_backend) | git::https://github.com/canonical/loki-worker-k8s-operator//terraform | track/2 | +| [loki\_coordinator](#module\_loki\_coordinator) | git::https://github.com/canonical/loki-coordinator-k8s-operator//terraform | track/2 | +| [loki\_read](#module\_loki\_read) | git::https://github.com/canonical/loki-worker-k8s-operator//terraform | track/2 | +| [loki\_write](#module\_loki\_write) | git::https://github.com/canonical/loki-worker-k8s-operator//terraform | track/2 | ## Inputs diff --git a/terraform/mimir/README.md b/terraform/mimir/README.md index db9b069..3de85a9 100644 --- a/terraform/mimir/README.md +++ b/terraform/mimir/README.md @@ -16,10 +16,10 @@ This is a Terraform module facilitating the deployment of Mimir solution, using | Name | Source | Version | |------|--------|---------| -| [mimir\_backend](#module\_mimir\_backend) | git::https://github.com/canonical/mimir-worker-k8s-operator//terraform | n/a | -| [mimir\_coordinator](#module\_mimir\_coordinator) | git::https://github.com/canonical/mimir-coordinator-k8s-operator//terraform | n/a | -| [mimir\_read](#module\_mimir\_read) | git::https://github.com/canonical/mimir-worker-k8s-operator//terraform | n/a | -| [mimir\_write](#module\_mimir\_write) | git::https://github.com/canonical/mimir-worker-k8s-operator//terraform | n/a | +| [mimir\_backend](#module\_mimir\_backend) | git::https://github.com/canonical/mimir-worker-k8s-operator//terraform | track/2 | +| [mimir\_coordinator](#module\_mimir\_coordinator) | git::https://github.com/canonical/mimir-coordinator-k8s-operator//terraform | track/2 | +| [mimir\_read](#module\_mimir\_read) | git::https://github.com/canonical/mimir-worker-k8s-operator//terraform | track/2 | +| [mimir\_write](#module\_mimir\_write) | git::https://github.com/canonical/mimir-worker-k8s-operator//terraform | track/2 | ## Inputs From 48867781afa1c0867f8c30618a111d9aabbc6313 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Tue, 17 Feb 2026 10:18:34 -0500 Subject: [PATCH 5/5] chore --- terraform/cos-lite/variables.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/terraform/cos-lite/variables.tf b/terraform/cos-lite/variables.tf index 101e696..19c5e47 100644 --- a/terraform/cos-lite/variables.tf +++ b/terraform/cos-lite/variables.tf @@ -16,8 +16,8 @@ variable "channel" { default = "2/stable" validation { - condition = split("/", var.channel)[0] == split("/", "2/stable")[0] - error_message = "The track (the part before the '/') of the channel must match the default." + condition = startswith(var.channel, "2/") + error_message = "The track of the channel must be '2/'. e.g. '2/stable'." } }