diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml index 52ba8f66..73ae12c8 100644 --- a/.github/workflows/terraform.yml +++ b/.github/workflows/terraform.yml @@ -44,6 +44,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 066c6794..463de799 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: @@ -35,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")] @@ -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}" diff --git a/terraform/cos-lite/README.md b/terraform/cos-lite/README.md index 3e3a8a69..b9ac46c7 100644 --- a/terraform/cos-lite/README.md +++ b/terraform/cos-lite/README.md @@ -27,7 +27,7 @@ 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 individual channels) deployed from | `string` | `"dev/edge"` | 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 |
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 00000000..5f38c6a9
--- /dev/null
+++ b/terraform/cos-lite/tests/channel_validation.tftest.hcl
@@ -0,0 +1,38 @@
+mock_provider "juju" {}
+
+variables {
+ model_uuid = "00000000-0000-0000-0000-000000000000"
+}
+
+# ---Happy path---
+
+run "valid_channel_stable" {
+ command = plan
+ variables { channel = "dev/stable" }
+}
+
+run "valid_channel_candidate" {
+ command = plan
+ variables { channel = "dev/candidate" }
+}
+
+run "valid_channel_beta" {
+ command = plan
+ variables { channel = "dev/beta" }
+}
+
+run "valid_channel_edge" {
+ command = plan
+ variables { channel = "dev/edge" }
+}
+
+# ---Failure path---
+# NOTE: Invalid risks (e.g. "dev/risk") are validated by the Juju provider at the
+# resource level inside child modules. Terraform test's expect_failures cannot
+# reference resources inside child modules, so we cannot assert on that here.
+
+run "invalid_channel_track_2" {
+ command = plan
+ variables { channel = "2/stable" }
+ expect_failures = [var.channel]
+}
diff --git a/terraform/cos-lite/variables.tf b/terraform/cos-lite/variables.tf
index 49b379e2..8767a8d5 100644
--- a/terraform/cos-lite/variables.tf
+++ b/terraform/cos-lite/variables.tf
@@ -11,8 +11,15 @@ locals {
}
variable "channel" {
- description = "Channel that the applications are (unless overwritten by external_channels) deployed from"
+ description = "Channel that the applications are (unless overwritten by individual channels) deployed from"
type = string
+ default = "dev/edge"
+
+ validation {
+ # the TF Juju provider correctly identifies invalid risks; no need to validate it
+ condition = startswith(var.channel, "dev/")
+ error_message = "The track of the channel must be 'dev/'. e.g. 'dev/edge'."
+ }
}
variable "model_uuid" {
diff --git a/terraform/cos/README.md b/terraform/cos/README.md
index d8033329..bae37f57 100644
--- a/terraform/cos/README.md
+++ b/terraform/cos/README.md
@@ -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 individual channels) deployed from | `string` | `"dev/edge"` | 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 |
@@ -54,7 +54,7 @@ This is a Terraform module facilitating the deployment of the COS solution, usin
| [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 |
| [tempo\_bucket](#input\_tempo\_bucket) | Tempo bucket name | `string` | `"tempo"` | no |
| [tempo\_coordinator](#input\_tempo\_coordinator) | Application configuration for Tempo Coordinator. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application | object({
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 3)
}) | `{}` | no |
-| [tempo\_worker](#input\_tempo\_worker) | Application configuration for all Tempo workers. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application | object({
querier_config = optional(map(string), {})
query_frontend_config = optional(map(string), {})
ingester_config = optional(map(string), {})
distributor_config = optional(map(string), {})
compactor_config = optional(map(string), {})
metrics_generator_config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
compactor_units = optional(number, 3)
distributor_units = optional(number, 3)
ingester_units = optional(number, 3)
metrics_generator_units = optional(number, 3)
querier_units = optional(number, 3)
query_frontend_units = optional(number, 3)
}) | `{}` | no |
+| [tempo\_worker](#input\_tempo\_worker) | Application configuration for all Tempo workers. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application | object({
querier_config = optional(map(string), {})
query_frontend_config = optional(map(string), {})
ingester_config = optional(map(string), {})
distributor_config = optional(map(string), {})
compactor_config = optional(map(string), {})
metrics_generator_config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
compactor_worker_storage_directives = optional(map(string), {})
distributor_worker_storage_directives = optional(map(string), {})
ingester_worker_storage_directives = optional(map(string), {})
metrics_generator_worker_storage_directives = optional(map(string), {})
querier_worker_storage_directives = optional(map(string), {})
query_frontend_worker_storage_directives = optional(map(string), {})
compactor_units = optional(number, 3)
distributor_units = optional(number, 3)
ingester_units = optional(number, 3)
metrics_generator_units = optional(number, 3)
querier_units = optional(number, 3)
query_frontend_units = optional(number, 3)
}) | `{}` | 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 |
## Outputs
diff --git a/terraform/cos/tests/channel_validation.tftest.hcl b/terraform/cos/tests/channel_validation.tftest.hcl
new file mode 100644
index 00000000..c2fdf469
--- /dev/null
+++ b/terraform/cos/tests/channel_validation.tftest.hcl
@@ -0,0 +1,41 @@
+mock_provider "juju" {}
+
+variables {
+ model_uuid = "00000000-0000-0000-0000-000000000000"
+ s3_endpoint = "foo"
+ s3_access_key = "foo"
+ s3_secret_key = "foo"
+}
+
+# ---Happy path---
+
+run "valid_channel_stable" {
+ command = plan
+ variables { channel = "dev/stable" }
+}
+
+run "valid_channel_candidate" {
+ command = plan
+ variables { channel = "dev/candidate" }
+}
+
+run "valid_channel_beta" {
+ command = plan
+ variables { channel = "dev/beta" }
+}
+
+run "valid_channel_edge" {
+ command = plan
+ variables { channel = "dev/edge" }
+}
+
+# ---Failure path---
+# NOTE: Invalid risks (e.g. "dev/risk") are validated by the Juju provider at the
+# resource level inside child modules. Terraform test's expect_failures cannot
+# reference resources inside child modules, so we cannot assert on that here.
+
+run "invalid_channel_track_2" {
+ command = plan
+ variables { channel = "2/stable" }
+ expect_failures = [var.channel]
+}
diff --git a/terraform/cos/variables.tf b/terraform/cos/variables.tf
index 46150c31..7e770558 100644
--- a/terraform/cos/variables.tf
+++ b/terraform/cos/variables.tf
@@ -11,8 +11,15 @@ locals {
}
variable "channel" {
- description = "Channel that the applications are (unless overwritten by external_channels) deployed from"
+ description = "Channel that the applications are (unless overwritten by individual channels) deployed from"
type = string
+ default = "dev/edge"
+
+ validation {
+ # the TF Juju provider correctly identifies invalid risks; no need to validate it
+ condition = startswith(var.channel, "dev/")
+ error_message = "The track of the channel must be 'dev/'. e.g. 'dev/edge'."
+ }
}
variable "model_uuid" {
diff --git a/tests/integration/cos_lite/tls_external/test_upgrade_cos_lite_tls_external.py b/tests/integration/cos_lite/tls_external/test_upgrade_cos_lite_tls_external.py
index 9e028b0e..62a7555b 100644
--- a/tests/integration/cos_lite/tls_external/test_upgrade_cos_lite_tls_external.py
+++ b/tests/integration/cos_lite/tls_external/test_upgrade_cos_lite_tls_external.py
@@ -25,7 +25,7 @@ def test_deploy_from_track(
# GIVEN a module deployed from track n-1
tf_manager.init(TRACK_2_TF_FILE)
tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model)
- wait_for_active_idle_without_error([ca_model, cos_model])
+ wait_for_active_idle_without_error([ca_model, cos_model], timeout=60*60)
tls_ctx = get_tls_context(tmp_path, ca_model, "self-signed-certificates")
catalogue_apps_are_reachable(cos_model, tls_ctx)
diff --git a/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py b/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py
index 340d81b5..d73b8bc2 100644
--- a/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py
+++ b/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py
@@ -25,7 +25,7 @@ def test_deploy_from_track(
# GIVEN a module deployed from track n-1
tf_manager.init(TRACK_2_TF_FILE)
tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model)
- wait_for_active_idle_without_error([ca_model, cos_model])
+ wait_for_active_idle_without_error([ca_model, cos_model], timeout=60*60)
tls_ctx = get_tls_context(tmp_path, ca_model, "self-signed-certificates")
catalogue_apps_are_reachable(cos_model, tls_ctx)
diff --git a/tests/integration/cos_lite/tls_internal/test_upgrade_cos_lite_tls_internal.py b/tests/integration/cos_lite/tls_internal/test_upgrade_cos_lite_tls_internal.py
index 76a132ec..125a8019 100644
--- a/tests/integration/cos_lite/tls_internal/test_upgrade_cos_lite_tls_internal.py
+++ b/tests/integration/cos_lite/tls_internal/test_upgrade_cos_lite_tls_internal.py
@@ -22,7 +22,7 @@ def test_deploy_from_track(tf_manager, cos_model: jubilant.Juju):
# GIVEN a module deployed from track n-1
tf_manager.init(TRACK_2_TF_FILE)
tf_manager.apply(model=cos_model.model)
- wait_for_active_idle_without_error([cos_model])
+ wait_for_active_idle_without_error([cos_model], timeout=60*60)
catalogue_apps_are_reachable(cos_model)
diff --git a/tests/integration/cos_lite/tls_none/test_upgrade_cos_lite_tls_none.py b/tests/integration/cos_lite/tls_none/test_upgrade_cos_lite_tls_none.py
index 1a8352c5..3150c1d3 100644
--- a/tests/integration/cos_lite/tls_none/test_upgrade_cos_lite_tls_none.py
+++ b/tests/integration/cos_lite/tls_none/test_upgrade_cos_lite_tls_none.py
@@ -22,7 +22,7 @@ def test_deploy_from_track(tf_manager, cos_model: jubilant.Juju):
# GIVEN a module deployed from track n-1
tf_manager.init(TRACK_2_TF_FILE)
tf_manager.apply(model=cos_model.model)
- wait_for_active_idle_without_error([cos_model])
+ wait_for_active_idle_without_error([cos_model], timeout=60*60)
catalogue_apps_are_reachable(cos_model)