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)