Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/terraform.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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")]
Expand All @@ -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}"
2 changes: 1 addition & 1 deletion terraform/cos-lite/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ This is a Terraform module facilitating the deployment of the COS Lite solution,
|------|-------------|------|---------|:--------:|
| <a name="input_alertmanager"></a> [alertmanager](#input\_alertmanager) | Application configuration for Alertmanager. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application | <pre>object({<br/> app_name = optional(string, "alertmanager")<br/> config = optional(map(string), {})<br/> constraints = optional(string, "arch=amd64")<br/> revision = optional(number, null)<br/> storage_directives = optional(map(string), {})<br/> units = optional(number, 1)<br/> })</pre> | `{}` | no |
| <a name="input_catalogue"></a> [catalogue](#input\_catalogue) | Application configuration for Catalogue. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application | <pre>object({<br/> app_name = optional(string, "catalogue")<br/> config = optional(map(string), {})<br/> constraints = optional(string, "arch=amd64")<br/> revision = optional(number, null)<br/> storage_directives = optional(map(string), {})<br/> units = optional(number, 1)<br/> })</pre> | `{}` | no |
| <a name="input_channel"></a> [channel](#input\_channel) | Channel that the applications are (unless overwritten by external\_channels) deployed from | `string` | n/a | yes |
| <a name="input_channel"></a> [channel](#input\_channel) | Channel that the applications are (unless overwritten by individual channels) deployed from | `string` | `"dev/edge"` | no |
| <a name="input_external_ca_cert_offer_url"></a> [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 |
| <a name="input_external_certificates_offer_url"></a> [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 |
| <a name="input_grafana"></a> [grafana](#input\_grafana) | Application configuration for Grafana. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application | <pre>object({<br/> app_name = optional(string, "grafana")<br/> config = optional(map(string), {})<br/> constraints = optional(string, "arch=amd64")<br/> revision = optional(number, null)<br/> storage_directives = optional(map(string), {})<br/> units = optional(number, 1)<br/> })</pre> | `{}` | no |
Expand Down
38 changes: 38 additions & 0 deletions terraform/cos-lite/tests/channel_validation.tftest.hcl
Original file line number Diff line number Diff line change
@@ -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]
}
9 changes: 8 additions & 1 deletion terraform/cos-lite/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down
4 changes: 2 additions & 2 deletions terraform/cos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ This is a Terraform module facilitating the deployment of the COS solution, usin
| <a name="input_alertmanager"></a> [alertmanager](#input\_alertmanager) | Application configuration for Alertmanager. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application | <pre>object({<br/> app_name = optional(string, "alertmanager")<br/> config = optional(map(string), {})<br/> constraints = optional(string, "arch=amd64")<br/> revision = optional(number, null)<br/> storage_directives = optional(map(string), {})<br/> units = optional(number, 1)<br/> })</pre> | `{}` | no |
| <a name="input_anti_affinity"></a> [anti\_affinity](#input\_anti\_affinity) | Enable anti-affinity constraints across all HA modules (Mimir, Loki, Tempo) | `bool` | `true` | no |
| <a name="input_catalogue"></a> [catalogue](#input\_catalogue) | Application configuration for Catalogue. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application | <pre>object({<br/> app_name = optional(string, "catalogue")<br/> config = optional(map(string), {})<br/> constraints = optional(string, "arch=amd64")<br/> revision = optional(number, null)<br/> storage_directives = optional(map(string), {})<br/> units = optional(number, 1)<br/> })</pre> | `{}` | no |
| <a name="input_channel"></a> [channel](#input\_channel) | Channel that the applications are (unless overwritten by external\_channels) deployed from | `string` | n/a | yes |
| <a name="input_channel"></a> [channel](#input\_channel) | Channel that the applications are (unless overwritten by individual channels) deployed from | `string` | `"dev/edge"` | no |
| <a name="input_cloud"></a> [cloud](#input\_cloud) | Kubernetes cloud or environment where this COS module will be deployed (e.g self-managed, aws) | `string` | `"self-managed"` | no |
| <a name="input_external_ca_cert_offer_url"></a> [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 |
| <a name="input_external_certificates_offer_url"></a> [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 |
Expand All @@ -54,7 +54,7 @@ This is a Terraform module facilitating the deployment of the COS solution, usin
| <a name="input_ssc"></a> [ssc](#input\_ssc) | Application configuration for Self-signed-certificates. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application | <pre>object({<br/> app_name = optional(string, "ca")<br/> channel = optional(string, "1/stable")<br/> config = optional(map(string), {})<br/> constraints = optional(string, "arch=amd64")<br/> revision = optional(number, null)<br/> storage_directives = optional(map(string), {})<br/> units = optional(number, 1)<br/> })</pre> | `{}` | no |
| <a name="input_tempo_bucket"></a> [tempo\_bucket](#input\_tempo\_bucket) | Tempo bucket name | `string` | `"tempo"` | no |
| <a name="input_tempo_coordinator"></a> [tempo\_coordinator](#input\_tempo\_coordinator) | Application configuration for Tempo Coordinator. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application | <pre>object({<br/> config = optional(map(string), {})<br/> constraints = optional(string, "arch=amd64")<br/> revision = optional(number, null)<br/> storage_directives = optional(map(string), {})<br/> units = optional(number, 3)<br/> })</pre> | `{}` | no |
| <a name="input_tempo_worker"></a> [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 | <pre>object({<br/> querier_config = optional(map(string), {})<br/> query_frontend_config = optional(map(string), {})<br/> ingester_config = optional(map(string), {})<br/> distributor_config = optional(map(string), {})<br/> compactor_config = optional(map(string), {})<br/> metrics_generator_config = optional(map(string), {})<br/> constraints = optional(string, "arch=amd64")<br/> revision = optional(number, null)<br/> storage_directives = optional(map(string), {})<br/> compactor_units = optional(number, 3)<br/> distributor_units = optional(number, 3)<br/> ingester_units = optional(number, 3)<br/> metrics_generator_units = optional(number, 3)<br/> querier_units = optional(number, 3)<br/> query_frontend_units = optional(number, 3)<br/> })</pre> | `{}` | no |
| <a name="input_tempo_worker"></a> [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 | <pre>object({<br/> querier_config = optional(map(string), {})<br/> query_frontend_config = optional(map(string), {})<br/> ingester_config = optional(map(string), {})<br/> distributor_config = optional(map(string), {})<br/> compactor_config = optional(map(string), {})<br/> metrics_generator_config = optional(map(string), {})<br/> constraints = optional(string, "arch=amd64")<br/> revision = optional(number, null)<br/> compactor_worker_storage_directives = optional(map(string), {})<br/> distributor_worker_storage_directives = optional(map(string), {})<br/> ingester_worker_storage_directives = optional(map(string), {})<br/> metrics_generator_worker_storage_directives = optional(map(string), {})<br/> querier_worker_storage_directives = optional(map(string), {})<br/> query_frontend_worker_storage_directives = optional(map(string), {})<br/> compactor_units = optional(number, 3)<br/> distributor_units = optional(number, 3)<br/> ingester_units = optional(number, 3)<br/> metrics_generator_units = optional(number, 3)<br/> querier_units = optional(number, 3)<br/> query_frontend_units = optional(number, 3)<br/> })</pre> | `{}` | no |
| <a name="input_traefik"></a> [traefik](#input\_traefik) | Application configuration for Traefik. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application | <pre>object({<br/> app_name = optional(string, "traefik")<br/> channel = optional(string, "latest/stable")<br/> config = optional(map(string), {})<br/> constraints = optional(string, "arch=amd64")<br/> revision = optional(number, null)<br/> storage_directives = optional(map(string), {})<br/> units = optional(number, 1)<br/> })</pre> | `{}` | no |

## Outputs
Expand Down
41 changes: 41 additions & 0 deletions terraform/cos/tests/channel_validation.tftest.hcl
Original file line number Diff line number Diff line change
@@ -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]
}
9 changes: 8 additions & 1 deletion terraform/cos/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
Loading