From 7c8fb40145951a5173ae6c0ec77cb8dc1d37f9ed Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Sun, 17 May 2026 16:16:39 +0200 Subject: [PATCH 01/11] overhaul for mesh --- terraform/cos-dev/applications.tf | 13 +- terraform/cos-dev/integrations.tf | 24 +- terraform/cos-dev/locals.tf | 4 +- terraform/cos-dev/offers.tf | 14 ++ terraform/cos-dev/outputs.tf | 32 +-- .../tests/conditional_ingress.tftest.hcl | 227 +----------------- terraform/cos-dev/tests/mesh.tftest.hcl | 97 ++++++++ .../cos-dev/tests/mesh_enabled.tftest.hcl | 65 ----- .../cos-dev/tests/reverse_proxy.tftest.hcl | 63 +++++ .../tests/topology_and_storage.tftest.hcl | 16 +- terraform/cos-dev/variables.tf | 106 ++++---- 11 files changed, 287 insertions(+), 374 deletions(-) create mode 100644 terraform/cos-dev/tests/mesh.tftest.hcl delete mode 100644 terraform/cos-dev/tests/mesh_enabled.tftest.hcl create mode 100644 terraform/cos-dev/tests/reverse_proxy.tftest.hcl diff --git a/terraform/cos-dev/applications.tf b/terraform/cos-dev/applications.tf index 24518092..c2e8ca59 100644 --- a/terraform/cos-dev/applications.tf +++ b/terraform/cos-dev/applications.tf @@ -36,7 +36,7 @@ module "grafana" { } module "istio-ingress" { - count = var.mesh_enabled ? 1 : 0 + count = var.mesh.enabled ? 1 : 0 source = "git::https://github.com/canonical/istio-ingress-k8s-operator//terraform" app_name = var.catalogue.app_name channel = local.channels.catalogue @@ -49,7 +49,7 @@ module "istio-ingress" { } module "istio-beacon" { - count = var.mesh_enabled ? 1 : 0 + count = var.mesh.enabled ? 1 : 0 source = "git::https://github.com/canonical/istio-beacon-k8s-operator//terraform" app_name = var.catalogue.app_name channel = local.channels.catalogue @@ -235,7 +235,7 @@ module "seaweedfs" { } module "ssc" { - count = var.internal_tls ? 1 : 0 + count = var.reverse_proxy.enabled ? 1 : 0 source = "git::https://github.com/canonical/self-signed-certificates-operator//terraform" app_name = var.ssc.app_name channel = local.channels.ssc @@ -366,6 +366,7 @@ module "tempo_worker_metrics_generator" { } module "traefik" { + count = var.reverse_proxy.enabled ? 1 : 0 source = "git::https://github.com/canonical/traefik-k8s-operator//terraform" app_name = var.traefik.app_name channel = local.channels.traefik @@ -402,7 +403,7 @@ resource "juju_application" "s3_integrator_loki" { count = var.storage_backend == "s3" ? 1 : 0 config = merge({ endpoint = var.s3_endpoint - bucket = var.loki_bucket + bucket = var.s3_buckets.loki credentials = "secret:${juju_secret.loki_s3_credentials[0].secret_id}" }, var.s3_integrator.config) constraints = var.s3_integrator.constraints @@ -441,7 +442,7 @@ resource "juju_application" "s3_integrator_mimir" { count = var.storage_backend == "s3" ? 1 : 0 config = merge({ endpoint = var.s3_endpoint - bucket = var.mimir_bucket + bucket = var.s3_buckets.mimir credentials = "secret:${juju_secret.mimir_s3_credentials[0].secret_id}" }, var.s3_integrator.config) constraints = var.s3_integrator.constraints @@ -480,7 +481,7 @@ resource "juju_application" "s3_integrator_tempo" { count = var.storage_backend == "s3" ? 1 : 0 config = merge({ endpoint = var.s3_endpoint - bucket = var.tempo_bucket + bucket = var.s3_buckets.tempo credentials = "secret:${juju_secret.tempo_s3_credentials[0].secret_id}" }, var.s3_integrator.config) constraints = var.s3_integrator.constraints diff --git a/terraform/cos-dev/integrations.tf b/terraform/cos-dev/integrations.tf index 9efba9a7..82f9b721 100644 --- a/terraform/cos-dev/integrations.tf +++ b/terraform/cos-dev/integrations.tf @@ -649,7 +649,7 @@ resource "juju_integration" "ingress" { app_name = module.mimir_coordinator.app_name endpoint = "ingress" } - } : k => v if var.ingress[k] && !var.mesh_enabled + } : k => v if var.ingress[k] && var.reverse_proxy.enabled } model_uuid = var.model_uuid @@ -666,7 +666,7 @@ resource "juju_integration" "ingress" { } resource "juju_integration" "grafana_ingress" { - count = var.ingress.grafana && !var.mesh_enabled ? 1 : 0 + count = var.ingress.grafana && var.reverse_proxy.enabled ? 1 : 0 model_uuid = var.model_uuid @@ -694,7 +694,7 @@ resource "juju_integration" "traefik_route" { app_name = module.tempo_coordinator.app_name endpoint = module.tempo_coordinator.requires.ingress } - } : k => v if var.ingress[k] && !var.mesh_enabled + } : k => v if var.ingress[k] && var.reverse_proxy.enabled } model_uuid = var.model_uuid @@ -729,7 +729,7 @@ resource "juju_integration" "istio_ingress" { app_name = module.mimir_coordinator.app_name endpoint = "ingress" } - } : k => v if var.ingress[k] && var.mesh_enabled + } : k => v if var.ingress[k] && var.mesh.enabled } model_uuid = var.model_uuid @@ -746,7 +746,7 @@ resource "juju_integration" "istio_ingress" { } resource "juju_integration" "grafana_istio_ingress" { - count = var.ingress.grafana && var.mesh_enabled ? 1 : 0 + count = var.ingress.grafana && var.mesh.enabled ? 1 : 0 model_uuid = var.model_uuid @@ -774,7 +774,7 @@ resource "juju_integration" "istio_ingress_route" { app_name = module.tempo_coordinator.app_name endpoint = module.tempo_coordinator.requires.ingress } - } : k => v if var.ingress[k] && var.mesh_enabled + } : k => v if var.ingress[k] && var.mesh.enabled } model_uuid = var.model_uuid @@ -809,7 +809,7 @@ resource "juju_integration" "opentelemetry_collector_mimir_metrics" { # -------------- # Certificate Integrations -------------- resource "juju_integration" "internal_certificates" { - for_each = var.internal_tls ? { + for_each = var.reverse_proxy.enabled ? { alertmanager = { app_name = module.alertmanager.app_name endpoint = module.alertmanager.requires.certificates @@ -854,7 +854,7 @@ resource "juju_integration" "internal_certificates" { } resource "juju_integration" "traefik_receive_ca_certificate" { - count = var.internal_tls ? 1 : 0 + count = var.reverse_proxy.enabled ? 1 : 0 model_uuid = var.model_uuid application { @@ -871,7 +871,7 @@ resource "juju_integration" "traefik_receive_ca_certificate" { # -------------- # Provided by an external CA -------------- resource "juju_integration" "external_traefik_certificates" { - count = local.tls_termination ? 1 : 0 + count = local.reverse_proxy_termination ? 1 : 0 model_uuid = var.model_uuid application { @@ -885,7 +885,7 @@ resource "juju_integration" "external_traefik_certificates" { } resource "juju_integration" "external_grafana_ca_cert" { - count = local.tls_termination ? 1 : 0 + count = local.reverse_proxy_termination ? 1 : 0 model_uuid = var.model_uuid application { @@ -899,7 +899,7 @@ resource "juju_integration" "external_grafana_ca_cert" { } resource "juju_integration" "external_otelcol_ca_cert" { - count = local.tls_termination ? 1 : 0 + count = local.reverse_proxy_termination ? 1 : 0 model_uuid = var.model_uuid application { @@ -945,7 +945,7 @@ resource "juju_integration" "traces_and_metrics_correlation" { # -------------- # Service Mesh --------------------- resource "juju_integration" "istio_beacon" { - for_each = var.mesh_enabled ? { + for_each = var.mesh.enabled ? { alertmanager = { app_name = module.alertmanager.app_name endpoint = module.alertmanager.requires.service_mesh diff --git a/terraform/cos-dev/locals.tf b/terraform/cos-dev/locals.tf index cd9c490b..b821a6a0 100644 --- a/terraform/cos-dev/locals.tf +++ b/terraform/cos-dev/locals.tf @@ -1,6 +1,6 @@ locals { - tls_termination = var.external_certificates_offer_url != null ? true : false - traefik_base = "ubuntu@20.04" + reverse_proxy_termination = var.reverse_proxy.enabled && var.reverse_proxy.cmr_urls.certificates != null ? true : false + traefik_base = "ubuntu@20.04" tracks = { alertmanager = "dev" catalogue = "dev" diff --git a/terraform/cos-dev/offers.tf b/terraform/cos-dev/offers.tf index 37e047c1..7ea4df9d 100644 --- a/terraform/cos-dev/offers.tf +++ b/terraform/cos-dev/offers.tf @@ -38,6 +38,7 @@ resource "juju_offer" "tempo_tracing" { # -------------- # CMR Mesh offers -------------- # resource "juju_offer" "alertmanager_provide_cmr_mesh" { + count = var.mesh.enabled ? 1 : 0 name = "alertmanager-provide-cmr-mesh" model_uuid = var.model_uuid application_name = module.alertmanager.app_name @@ -45,6 +46,7 @@ resource "juju_offer" "alertmanager_provide_cmr_mesh" { } resource "juju_offer" "alertmanager_require_cmr_mesh" { + count = var.mesh.enabled ? 1 : 0 name = "alertmanager-require-cmr-mesh" model_uuid = var.model_uuid application_name = module.alertmanager.app_name @@ -52,6 +54,7 @@ resource "juju_offer" "alertmanager_require_cmr_mesh" { } resource "juju_offer" "catalogue_provide_cmr_mesh" { + count = var.mesh.enabled ? 1 : 0 name = "catalogue-provide-cmr-mesh" model_uuid = var.model_uuid application_name = module.catalogue.app_name @@ -59,6 +62,7 @@ resource "juju_offer" "catalogue_provide_cmr_mesh" { } resource "juju_offer" "catalogue_require_cmr_mesh" { + count = var.mesh.enabled ? 1 : 0 name = "catalogue-require-cmr-mesh" model_uuid = var.model_uuid application_name = module.catalogue.app_name @@ -66,6 +70,7 @@ resource "juju_offer" "catalogue_require_cmr_mesh" { } resource "juju_offer" "grafana_provide_cmr_mesh" { + count = var.mesh.enabled ? 1 : 0 name = "grafana-provide-cmr-mesh" model_uuid = var.model_uuid application_name = module.grafana.app_name @@ -75,6 +80,7 @@ resource "juju_offer" "grafana_provide_cmr_mesh" { } resource "juju_offer" "grafana_require_cmr_mesh" { + count = var.mesh.enabled ? 1 : 0 name = "grafana-require-cmr-mesh" model_uuid = var.model_uuid application_name = module.grafana.app_name @@ -84,6 +90,7 @@ resource "juju_offer" "grafana_require_cmr_mesh" { } resource "juju_offer" "loki_provide_cmr_mesh" { + count = var.mesh.enabled ? 1 : 0 name = "loki-provide-cmr-mesh" model_uuid = var.model_uuid application_name = module.loki_coordinator.app_name @@ -91,6 +98,7 @@ resource "juju_offer" "loki_provide_cmr_mesh" { } resource "juju_offer" "loki_require_cmr_mesh" { + count = var.mesh.enabled ? 1 : 0 name = "loki-require-cmr-mesh" model_uuid = var.model_uuid application_name = module.loki_coordinator.app_name @@ -98,6 +106,7 @@ resource "juju_offer" "loki_require_cmr_mesh" { } resource "juju_offer" "mimir_provide_cmr_mesh" { + count = var.mesh.enabled ? 1 : 0 name = "mimir-provide-cmr-mesh" model_uuid = var.model_uuid application_name = module.mimir_coordinator.app_name @@ -105,6 +114,7 @@ resource "juju_offer" "mimir_provide_cmr_mesh" { } resource "juju_offer" "mimir_require_cmr_mesh" { + count = var.mesh.enabled ? 1 : 0 name = "mimir-require-cmr-mesh" model_uuid = var.model_uuid application_name = module.mimir_coordinator.app_name @@ -112,6 +122,7 @@ resource "juju_offer" "mimir_require_cmr_mesh" { } resource "juju_offer" "opentelemetry_collector_provide_cmr_mesh" { + count = var.mesh.enabled ? 1 : 0 name = "opentelemetry-collector-provide-cmr-mesh" model_uuid = var.model_uuid application_name = module.opentelemetry_collector.app_name @@ -119,6 +130,7 @@ resource "juju_offer" "opentelemetry_collector_provide_cmr_mesh" { } resource "juju_offer" "opentelemetry_collector_require_cmr_mesh" { + count = var.mesh.enabled ? 1 : 0 name = "opentelemetry-collector-require-cmr-mesh" model_uuid = var.model_uuid application_name = module.opentelemetry_collector.app_name @@ -126,6 +138,7 @@ resource "juju_offer" "opentelemetry_collector_require_cmr_mesh" { } resource "juju_offer" "tempo_provide_cmr_mesh" { + count = var.mesh.enabled ? 1 : 0 name = "tempo-provide-cmr-mesh" model_uuid = var.model_uuid application_name = module.tempo_coordinator.app_name @@ -133,6 +146,7 @@ resource "juju_offer" "tempo_provide_cmr_mesh" { } resource "juju_offer" "tempo_require_cmr_mesh" { + count = var.mesh.enabled ? 1 : 0 name = "tempo-require-cmr-mesh" model_uuid = var.model_uuid application_name = module.tempo_coordinator.app_name diff --git a/terraform/cos-dev/outputs.tf b/terraform/cos-dev/outputs.tf index 74d663ba..733e3bdc 100644 --- a/terraform/cos-dev/outputs.tf +++ b/terraform/cos-dev/outputs.tf @@ -9,20 +9,20 @@ output "offers" { tempo_tracing = juju_offer.tempo_tracing # CMR Mesh - alertmanager_provide_cmr_mesh = juju_offer.alertmanager_provide_cmr_mesh - alertmanager_require_cmr_mesh = juju_offer.alertmanager_require_cmr_mesh - catalogue_provide_cmr_mesh = juju_offer.catalogue_provide_cmr_mesh - catalogue_require_cmr_mesh = juju_offer.catalogue_require_cmr_mesh - grafana_provide_cmr_mesh = juju_offer.grafana_provide_cmr_mesh - grafana_require_cmr_mesh = juju_offer.grafana_require_cmr_mesh - loki_provide_cmr_mesh = juju_offer.loki_provide_cmr_mesh - loki_require_cmr_mesh = juju_offer.loki_require_cmr_mesh - mimir_provide_cmr_mesh = juju_offer.mimir_provide_cmr_mesh - mimir_require_cmr_mesh = juju_offer.mimir_require_cmr_mesh - opentelemetry_collector_provide_cmr_mesh = juju_offer.opentelemetry_collector_provide_cmr_mesh - opentelemetry_collector_require_cmr_mesh = juju_offer.opentelemetry_collector_require_cmr_mesh - tempo_provide_cmr_mesh = juju_offer.tempo_provide_cmr_mesh - tempo_require_cmr_mesh = juju_offer.tempo_require_cmr_mesh + alertmanager_provide_cmr_mesh = try(juju_offer.alertmanager_provide_cmr_mesh[0], null) + alertmanager_require_cmr_mesh = try(juju_offer.alertmanager_require_cmr_mesh[0], null) + catalogue_provide_cmr_mesh = try(juju_offer.catalogue_provide_cmr_mesh[0], null) + catalogue_require_cmr_mesh = try(juju_offer.catalogue_require_cmr_mesh[0], null) + grafana_provide_cmr_mesh = try(juju_offer.grafana_provide_cmr_mesh[0], null) + grafana_require_cmr_mesh = try(juju_offer.grafana_require_cmr_mesh[0], null) + loki_provide_cmr_mesh = try(juju_offer.loki_provide_cmr_mesh[0], null) + loki_require_cmr_mesh = try(juju_offer.loki_require_cmr_mesh[0], null) + mimir_provide_cmr_mesh = try(juju_offer.mimir_provide_cmr_mesh[0], null) + mimir_require_cmr_mesh = try(juju_offer.mimir_require_cmr_mesh[0], null) + opentelemetry_collector_provide_cmr_mesh = try(juju_offer.opentelemetry_collector_provide_cmr_mesh[0], null) + opentelemetry_collector_require_cmr_mesh = try(juju_offer.opentelemetry_collector_require_cmr_mesh[0], null) + tempo_provide_cmr_mesh = try(juju_offer.tempo_provide_cmr_mesh[0], null) + tempo_require_cmr_mesh = try(juju_offer.tempo_require_cmr_mesh[0], null) } description = "All Juju offers which are exposed by this product module" } @@ -34,6 +34,8 @@ output "components" { alertmanager = module.alertmanager catalogue = module.catalogue grafana = module.grafana + grafana = try(module.istio_beacon[0], null) + grafana = try(module.istio_ingress[0], null) loki_coordinator = module.loki_coordinator loki_worker = try(module.loki_worker[0], null) loki_worker_backend = try(module.loki_worker_backend[0], null) @@ -55,7 +57,7 @@ output "components" { tempo_worker_distributor = try(module.tempo_worker_distributor[0], null) tempo_worker_compactor = try(module.tempo_worker_compactor[0], null) tempo_worker_metrics_generator = try(module.tempo_worker_metrics_generator[0], null) - traefik = module.traefik + traefik = try(module.traefik[0], null) } description = "All Terraform charm modules which make up this product module" } diff --git a/terraform/cos-dev/tests/conditional_ingress.tftest.hcl b/terraform/cos-dev/tests/conditional_ingress.tftest.hcl index bc9f5762..519bc9f0 100644 --- a/terraform/cos-dev/tests/conditional_ingress.tftest.hcl +++ b/terraform/cos-dev/tests/conditional_ingress.tftest.hcl @@ -4,31 +4,13 @@ variables { model_uuid = "00000000-0000-0000-0000-000000000000" } -# --- Default: all ingress integrations enabled --- +# TODO: This feature also depends on the x2 Traefik story, maybe reverse proxy is not the right name +# TODO: Do we need to remove offers / outputs TF conditionally? +# TODO: We need to keep the COS API the same across products: feature in COS, COS Lite, and COS Dev -run "default_ingress_all_enabled" { - command = plan - - assert { - condition = length(juju_integration.ingress) == 4 - error_message = "Expected 4 ingress integrations (alertmanager, catalogue, loki, mimir), got ${length(juju_integration.ingress)}" - } - - # Grafana uses a separate count-based resource due to lifecycle replace_triggered_by - assert { - condition = length(juju_integration.grafana_ingress) == 1 - error_message = "Expected 1 grafana_ingress integration, got ${length(juju_integration.grafana_ingress)}" - } - - assert { - condition = length(juju_integration.traefik_route) == 2 - error_message = "Expected 2 traefik_route integrations (opentelemetry_collector, tempo), got ${length(juju_integration.traefik_route)}" - } -} - -# --- All ingress disabled --- +# --- reverse proxy: enabled - all ingress disabled --- -run "ingress_all_disabled" { +run "reverse_proxy_ingress_disabled" { command = plan variables { @@ -59,45 +41,14 @@ run "ingress_all_disabled" { } } -# --- Only grafana exposed --- +# --- mesh: enabled - all ingress disabled --- -run "ingress_only_grafana" { - command = plan - - variables { - ingress = { - alertmanager = false - catalogue = false - grafana = true - loki = false - mimir = false - opentelemetry_collector = false - tempo = false - } - } - - assert { - condition = length(juju_integration.ingress) == 0 - error_message = "Expected 0 ingress integrations, got ${length(juju_integration.ingress)}" - } - - assert { - condition = length(juju_integration.grafana_ingress) == 1 - error_message = "Expected 1 grafana_ingress integration, got ${length(juju_integration.grafana_ingress)}" - } - - assert { - condition = length(juju_integration.traefik_route) == 0 - error_message = "Expected 0 traefik_route integrations, got ${length(juju_integration.traefik_route)}" - } -} - -# --- Only tempo exposed (traefik_route) --- - -run "ingress_only_tempo" { +run "mesh_ingress_disabled" { command = plan variables { + mesh = { enabled = true } + reverse_proxy = { enabled = false } ingress = { alertmanager = false catalogue = false @@ -105,176 +56,22 @@ run "ingress_only_tempo" { loki = false mimir = false opentelemetry_collector = false - tempo = true - } - } - - assert { - condition = length(juju_integration.ingress) == 0 - error_message = "Expected 0 ingress integrations, got ${length(juju_integration.ingress)}" - } - - assert { - condition = length(juju_integration.traefik_route) == 1 - error_message = "Expected 1 traefik_route integration (tempo), got ${length(juju_integration.traefik_route)}" - } - - assert { - condition = contains(keys(juju_integration.traefik_route), "tempo") - error_message = "Expected traefik_route to contain 'tempo' key" - } -} - -# --- Partial override: disable alertmanager and tempo --- - -run "ingress_partial_override" { - command = plan - - variables { - ingress = { - alertmanager = false - tempo = false - } - } - - assert { - condition = length(juju_integration.ingress) == 3 - error_message = "Expected 3 ingress integrations (catalogue, loki, mimir), got ${length(juju_integration.ingress)}" - } - - assert { - condition = !contains(keys(juju_integration.ingress), "alertmanager") - error_message = "Expected ingress to NOT contain 'alertmanager' key" - } - - assert { - condition = length(juju_integration.grafana_ingress) == 1 - error_message = "Expected 1 grafana_ingress integration, got ${length(juju_integration.grafana_ingress)}" - } - - assert { - condition = length(juju_integration.traefik_route) == 1 - error_message = "Expected 1 traefik_route integration (opentelemetry_collector), got ${length(juju_integration.traefik_route)}" - } - - assert { - condition = contains(keys(juju_integration.traefik_route), "opentelemetry_collector") - error_message = "Expected traefik_route to contain 'opentelemetry_collector' key" - } -} - -# --- mesh_enabled: all ingress via istio, none via traefik --- - -run "mesh_ingress_all_enabled" { - command = plan - - variables { - mesh_enabled = true - internal_tls = false - } - - # Traefik ingress resources should be empty - assert { - condition = length(juju_integration.ingress) == 0 - error_message = "Expected 0 traefik ingress integrations when mesh is enabled, got ${length(juju_integration.ingress)}" - } - - assert { - condition = length(juju_integration.grafana_ingress) == 0 - error_message = "Expected 0 grafana_ingress integrations when mesh is enabled, got ${length(juju_integration.grafana_ingress)}" - } - - assert { - condition = length(juju_integration.traefik_route) == 0 - error_message = "Expected 0 traefik_route integrations when mesh is enabled, got ${length(juju_integration.traefik_route)}" - } - - # Istio ingress resources should be populated - assert { - condition = length(juju_integration.istio_ingress) == 4 - error_message = "Expected 4 istio_ingress integrations (alertmanager, catalogue, loki, mimir), got ${length(juju_integration.istio_ingress)}" - } - - assert { - condition = length(juju_integration.grafana_istio_ingress) == 1 - error_message = "Expected 1 grafana_istio_ingress integration, got ${length(juju_integration.grafana_istio_ingress)}" - } - - assert { - condition = length(juju_integration.istio_ingress_route) == 2 - error_message = "Expected 2 istio_ingress_route integrations (opentelemetry_collector, tempo), got ${length(juju_integration.istio_ingress_route)}" - } -} - -# --- mesh_enabled with partial ingress override --- - -run "mesh_ingress_partial" { - command = plan - - variables { - mesh_enabled = true - internal_tls = false - ingress = { - alertmanager = false - catalogue = true - grafana = false - loki = true - mimir = false - opentelemetry_collector = true tempo = false } } - # Traefik resources should all be empty - assert { - condition = length(juju_integration.ingress) == 0 - error_message = "Expected 0 traefik ingress integrations when mesh is enabled, got ${length(juju_integration.ingress)}" - } - - assert { - condition = length(juju_integration.grafana_ingress) == 0 - error_message = "Expected 0 grafana_ingress integrations when mesh is enabled" - } - - assert { - condition = length(juju_integration.traefik_route) == 0 - error_message = "Expected 0 traefik_route integrations when mesh is enabled" - } - - # Istio resources should respect the ingress toggles - assert { - condition = length(juju_integration.istio_ingress) == 2 - error_message = "Expected 2 istio_ingress integrations (catalogue, loki), got ${length(juju_integration.istio_ingress)}" - } - - assert { - condition = length(juju_integration.grafana_istio_ingress) == 0 - error_message = "Expected 0 grafana_istio_ingress integrations when grafana ingress is disabled" - } - - assert { - condition = length(juju_integration.istio_ingress_route) == 1 - error_message = "Expected 1 istio_ingress_route integration (opentelemetry_collector), got ${length(juju_integration.istio_ingress_route)}" - } -} - -# --- mesh disabled (default): no istio ingress resources --- - -run "no_mesh_no_istio_ingress" { - command = plan - assert { condition = length(juju_integration.istio_ingress) == 0 - error_message = "Expected 0 istio_ingress integrations when mesh is disabled, got ${length(juju_integration.istio_ingress)}" + error_message = "Expected 0 istio_ingress integrations, got ${length(juju_integration.istio_ingress)}" } assert { condition = length(juju_integration.grafana_istio_ingress) == 0 - error_message = "Expected 0 grafana_istio_ingress integrations when mesh is disabled" + error_message = "Expected 0 grafana_istio_ingress integrations, got ${length(juju_integration.grafana_istio_ingress)}" } assert { condition = length(juju_integration.istio_ingress_route) == 0 - error_message = "Expected 0 istio_ingress_route integrations when mesh is disabled" + error_message = "Expected 0 istio_ingress_route integrations, got ${length(juju_integration.istio_ingress_route)}" } } diff --git a/terraform/cos-dev/tests/mesh.tftest.hcl b/terraform/cos-dev/tests/mesh.tftest.hcl new file mode 100644 index 00000000..c3ff57eb --- /dev/null +++ b/terraform/cos-dev/tests/mesh.tftest.hcl @@ -0,0 +1,97 @@ +mock_provider "juju" {} + +variables { + model_uuid = "00000000-0000-0000-0000-000000000000" +} + +# --- mesh and reverse_proxy enabled: validation error --- + +run "mesh_and_reverse_proxy_enabled_fails" { + command = plan + + variables { + mesh = { enabled = false } + reverse_proxy = { enabled = true } + } + + expect_failures = [var.mesh.enabled] +} + +# --- mesh and reverse_proxy disabled: no validation error --- + +run "mesh_and_reverse_proxy_disabled" { + command = plan + + variables { + mesh = { enabled = false } + reverse_proxy = { enabled = false } + } +} + +# --- default: mesh disabled - no ingress via istio --- + +run "mesh_disabled" { + command = plan + + assert { + condition = length(module.istio-beacon) == 0 + error_message = "Expected no istio-beacon module when the mesh is disabled" + } + + assert { + condition = length(module.istio-ingress) == 0 + error_message = "Expected no istio-ingress module when the mesh is disabled" + } + + assert { + condition = length(juju_integration.istio_beacon) == 0 + error_message = "Expected no istio_beacon integrations when the mesh is disabled" + } + + assert { + condition = length(juju_integration.grafana_istio_ingress) == 0 + error_message = "Expected no grafana_istio_beacon integrations when the mesh is disabled" + } + + assert { + condition = length(juju_integration.istio_ingress_route) == 0 + error_message = "Expected no istio_ingress_route integrations when the mesh is disabled" + } +} + +# --- mesh enabled: ingress via istio --- + +run "mesh_enabled" { + command = plan + + variables { + mesh = { enabled = true } + reverse_proxy = { enabled = false } + } + + assert { + condition = length(module.istio-beacon) == 1 + error_message = "Expected istio-beacon module when the mesh is enabled" + } + + assert { + condition = length(module.istio-ingress) == 1 + error_message = "Expected istio-ingress module when the mesh is enabled" + } + + assert { + condition = length(juju_integration.istio_ingress) == 4 + error_message = "Expected 4 istio_ingress integrations (alertmanager, catalogue, loki, mimir), got ${length(juju_integration.istio_ingress)}" + } + + # Grafana uses a separate count-based resource due to lifecycle replace_triggered_by + assert { + condition = length(juju_integration.grafana_istio_ingress) == 1 + error_message = "Expected 1 grafana_istio_ingress integration, got ${length(juju_integration.grafana_istio_ingress)}" + } + + assert { + condition = length(juju_integration.istio_ingress_route) == 2 + error_message = "Expected 2 istio_ingress_route integrations (opentelemetry_collector, tempo), got ${length(juju_integration.istio_ingress_route)}" + } +} diff --git a/terraform/cos-dev/tests/mesh_enabled.tftest.hcl b/terraform/cos-dev/tests/mesh_enabled.tftest.hcl deleted file mode 100644 index 54f7448b..00000000 --- a/terraform/cos-dev/tests/mesh_enabled.tftest.hcl +++ /dev/null @@ -1,65 +0,0 @@ -mock_provider "juju" {} - -variables { - model_uuid = "00000000-0000-0000-0000-000000000000" -} - -# --- mesh_enabled=false (default): no istio modules or integrations deployed --- - -run "mesh_disabled_by_default" { - command = plan - - assert { - condition = length(module.istio-beacon) == 0 - error_message = "Expected no istio-beacon module when mesh is disabled" - } - - assert { - condition = length(module.istio-ingress) == 0 - error_message = "Expected no istio-ingress module when mesh is disabled" - } - - assert { - condition = length(juju_integration.istio_beacon) == 0 - error_message = "Expected no istio_beacon integrations when mesh is disabled" - } -} - -# --- mesh_enabled=true with internal_tls=false: istio modules and integrations deployed --- - -run "mesh_enabled_without_tls" { - command = plan - - variables { - mesh_enabled = true - internal_tls = false - } - - assert { - condition = length(module.istio-beacon) == 1 - error_message = "Expected istio-beacon module to be deployed when mesh is enabled" - } - - assert { - condition = length(module.istio-ingress) == 1 - error_message = "Expected istio-ingress module to be deployed when mesh is enabled" - } - - assert { - condition = length(juju_integration.istio_beacon) == 7 - error_message = "Expected 7 istio_beacon integrations (one per component) when mesh is enabled" - } -} - -# --- mesh_enabled=true with internal_tls=true (default): validation error --- - -run "mesh_enabled_with_internal_tls_fails" { - command = plan - - variables { - mesh_enabled = true - internal_tls = true - } - - expect_failures = [var.mesh_enabled] -} diff --git a/terraform/cos-dev/tests/reverse_proxy.tftest.hcl b/terraform/cos-dev/tests/reverse_proxy.tftest.hcl new file mode 100644 index 00000000..8e105460 --- /dev/null +++ b/terraform/cos-dev/tests/reverse_proxy.tftest.hcl @@ -0,0 +1,63 @@ +mock_provider "juju" {} + +variables { + model_uuid = "00000000-0000-0000-0000-000000000000" +} + +# --- default: reverse proxy enabled - ingress via traefik --- + +run "reverse_proxy_enabled" { + command = plan + + assert { + condition = length(module.traefik) == 0 + error_message = "Expected traefik module when the reverse proxy is enabled" + } + + assert { + condition = length(juju_integration.ingress) == 4 + error_message = "Expected 4 ingress integrations (alertmanager, catalogue, loki, mimir), got ${length(juju_integration.ingress)}" + } + + # Grafana uses a separate count-based resource due to lifecycle replace_triggered_by + assert { + condition = length(juju_integration.grafana_ingress) == 1 + error_message = "Expected 1 grafana_ingress integration, got ${length(juju_integration.grafana_ingress)}" + } + + assert { + condition = length(juju_integration.traefik_route) == 2 + error_message = "Expected 2 traefik_route integrations (opentelemetry_collector, tempo), got ${length(juju_integration.traefik_route)}" + } +} + +# --- reverse proxy disabled: no ingress via traefik --- + +run "reverse_proxy_disabled" { + command = plan + + variables { + mesh = { enabled = false } + reverse_proxy = { enabled = false } + } + + assert { + condition = length(module.traefik) == 0 + error_message = "Expected no traefik module when the reverse proxy is disabled" + } + + assert { + condition = length(juju_integration.istio_ingress) == 0 + error_message = "Expected 0 istio_ingress integrations when the reverse proxy is disabled, got ${length(juju_integration.istio_ingress)}" + } + + assert { + condition = length(juju_integration.grafana_istio_ingress) == 0 + error_message = "Expected 0 grafana_istio_ingress integrations when the reverse proxy is disabled" + } + + assert { + condition = length(juju_integration.istio_ingress_route) == 0 + error_message = "Expected 0 istio_ingress_route integrations when the reverse proxy is disabled" + } +} diff --git a/terraform/cos-dev/tests/topology_and_storage.tftest.hcl b/terraform/cos-dev/tests/topology_and_storage.tftest.hcl index ffb3b5c0..e29c3cdf 100644 --- a/terraform/cos-dev/tests/topology_and_storage.tftest.hcl +++ b/terraform/cos-dev/tests/topology_and_storage.tftest.hcl @@ -214,9 +214,11 @@ run "monolithic_s3" { s3_endpoint = "https://s3.example.com" s3_access_key = "access-key" s3_secret_key = "secret-key" - loki_bucket = "loki" - mimir_bucket = "mimir" - tempo_bucket = "tempo" + s3_buckets = { + loki = "loki" + mimir = "mimir" + tempo = "tempo" + } } assert { @@ -290,9 +292,11 @@ run "distributed_s3" { s3_endpoint = "https://s3.example.com" s3_access_key = "access-key" s3_secret_key = "secret-key" - loki_bucket = "loki" - mimir_bucket = "mimir" - tempo_bucket = "tempo" + s3_buckets = { + loki = "loki" + mimir = "mimir" + tempo = "tempo" + } } assert { diff --git a/terraform/cos-dev/variables.tf b/terraform/cos-dev/variables.tf index 9c5c6de2..1ee5b919 100644 --- a/terraform/cos-dev/variables.tf +++ b/terraform/cos-dev/variables.tf @@ -46,46 +46,50 @@ variable "storage_backend" { } } -# -------------- # Mesh configurations -------------- -variable "mesh_enabled" { - description = "Specify whether to enable the service mesh or not." - type = bool - default = false +# -------------- # Network configurations -------------- + +variable "mesh" { + description = "Configure the service mesh." + type = object({ + enabled = optional(bool, false) + cmr_urls = optional(object, { + # TODO: Consider making these null? + alermanager = "" + catalogue = "" + grafana = "" + loki = "" + mimir = "" + tempo = "" + }) + }) + default = {} validation { - condition = !(var.mesh_enabled && var.internal_tls) + condition = !(var.mesh.enabled && var.reverse_proxy.enabled) error_message = "mesh_enabled and internal_tls cannot both be enabled at the same time." } } -# -------------- # TLS configurations -------------- - -variable "internal_tls" { - description = "Specify whether to use TLS or not for internal COS communication. By default, TLS is enabled using self-signed-certificates" - type = bool - default = true -} - -variable "external_certificates_offer_url" { - description = "A Juju offer URL of a CA providing the 'tls_certificates' integration for Traefik to supply it with server certificates" - type = string - default = null +variable "reverse_proxy" { + description = "Configure the reverse proxy." + type = object({ + enabled = optional(bool, true) + cmr_urls = optional(object, { + certificates = "" + receive_ca_cert = "" + }) + }) + default = {} validation { condition = ( - (var.external_certificates_offer_url == null && var.external_ca_cert_offer_url == null) || - (var.external_certificates_offer_url != null && var.external_ca_cert_offer_url != null) + (var.reverse_proxy.cmr_urls.certificates == null && var.reverse_proxy.cmr_urls.receive_ca_cert == null) || + (var.reverse_proxy.cmr_urls.certificates != null && var.reverse_proxy.cmr_urls.receive_ca_cert != null) ) - error_message = "external_certificates_offer_url and external_ca_cert_offer_url must be supplied together (either both set or both null)." + error_message = "CMRs for certificates and receive_ca_cert must be supplied together (either both set or both null)." } } -variable "external_ca_cert_offer_url" { - description = "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." - type = string - default = null -} - # -------------- # Ingress configurations -------------- variable "ingress" { @@ -124,34 +128,18 @@ variable "s3_secret_key" { default = null } -variable "loki_bucket" { - description = "Loki S3 bucket name" - type = string - default = "loki" -} - -variable "mimir_bucket" { - description = "Mimir S3 bucket name" - type = string - default = "mimir" -} - -variable "tempo_bucket" { - description = "Tempo S3 bucket name" - type = string - default = "tempo" -} - -variable "s3_integrator" { +# TODO: Move the refactor efforts into a separate PR that the mesh team can branch from to get mesh merged: +# 1. buckets, reverse_proxy +# 2. I couldn't group s3_ because this has "sensitive" vars. Check if this is 100% not possible. +# What if we made the whole s3 var sensitive? +variable "s3_buckets" { + description = "S3 bucket names for components" type = object({ - config = optional(map(string), {}) - constraints = optional(string, "arch=amd64") - revision = optional(number, null) - storage_directives = optional(map(string), {}) - units = optional(number, 1) + loki = optional(string, "loki") + mimir = optional(string, "mimir") + tempo = optional(string, "tempo") }) - default = {} - description = "Application configuration shared by all S3-integrators (one deployed per coordinated worker). For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application" + default = {} } # -------------- # Application configurations -------------- @@ -334,6 +322,18 @@ variable "ssc" { description = "Application configuration for Self-signed-certificates. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application" } +variable "s3_integrator" { + type = object({ + config = optional(map(string), {}) + constraints = optional(string, "arch=amd64") + revision = optional(number, null) + storage_directives = optional(map(string), {}) + units = optional(number, 1) + }) + default = {} + description = "Application configuration shared by all S3-integrators (one deployed per coordinated worker). For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application" +} + variable "tempo_coordinator" { type = object({ app_name = optional(string, "tempo") From 8ea44dc5d7e26ed1a1b62d9d7d33e1528387773f Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Sun, 17 May 2026 16:34:01 +0200 Subject: [PATCH 02/11] chore --- .../tests/conditional_ingress.tftest.hcl | 1 + terraform/cos-dev/variables.tf | 27 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/terraform/cos-dev/tests/conditional_ingress.tftest.hcl b/terraform/cos-dev/tests/conditional_ingress.tftest.hcl index 519bc9f0..faef5fbd 100644 --- a/terraform/cos-dev/tests/conditional_ingress.tftest.hcl +++ b/terraform/cos-dev/tests/conditional_ingress.tftest.hcl @@ -7,6 +7,7 @@ variables { # TODO: This feature also depends on the x2 Traefik story, maybe reverse proxy is not the right name # TODO: Do we need to remove offers / outputs TF conditionally? # TODO: We need to keep the COS API the same across products: feature in COS, COS Lite, and COS Dev +# TODO: For the conditional ingress story, do we support the same components between mesh and reverse proxy? If yes, then they can share the ingress var. Otherwise not, e.g. COS Lite # --- reverse proxy: enabled - all ingress disabled --- diff --git a/terraform/cos-dev/variables.tf b/terraform/cos-dev/variables.tf index 1ee5b919..7e466ff9 100644 --- a/terraform/cos-dev/variables.tf +++ b/terraform/cos-dev/variables.tf @@ -52,15 +52,15 @@ variable "mesh" { description = "Configure the service mesh." type = object({ enabled = optional(bool, false) - cmr_urls = optional(object, { - # TODO: Consider making these null? - alermanager = "" - catalogue = "" - grafana = "" - loki = "" - mimir = "" - tempo = "" - }) + cmr_urls = optional(object({ + alermanager = optional(string, null) + catalogue = optional(string, null) + grafana = optional(string, null) + loki = optional(string, null) + mimir = optional(string, null) + tempo = optional(string, null) + }), {} + ) }) default = {} @@ -74,10 +74,11 @@ variable "reverse_proxy" { description = "Configure the reverse proxy." type = object({ enabled = optional(bool, true) - cmr_urls = optional(object, { - certificates = "" - receive_ca_cert = "" - }) + cmr_urls = optional(object({ + certificates = optional(string, null) + receive_ca_cert = optional(string, null) + }), {} + ) }) default = {} From f9a7a7f54767c510aaf8924f1af04cf2bc0f3689 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Mon, 18 May 2026 04:44:33 +0200 Subject: [PATCH 03/11] chore --- terraform/cos-dev/applications.tf | 32 +++++++++---------- terraform/cos-dev/integrations.tf | 26 +++++++-------- terraform/cos-dev/locals.tf | 20 ++++++++---- terraform/cos-dev/outputs.tf | 4 +-- terraform/cos-dev/tests/mesh.tftest.hcl | 18 +++++------ .../cos-dev/tests/reverse_proxy.tftest.hcl | 2 +- terraform/cos-dev/upgrades.tf | 12 +++++++ 7 files changed, 66 insertions(+), 48 deletions(-) diff --git a/terraform/cos-dev/applications.tf b/terraform/cos-dev/applications.tf index c2e8ca59..304a3b8e 100644 --- a/terraform/cos-dev/applications.tf +++ b/terraform/cos-dev/applications.tf @@ -35,30 +35,30 @@ module "grafana" { replace_triggers = [terraform_data.grafana_litestream_resource.id] } -module "istio-ingress" { +module "istio_ingress" { count = var.mesh.enabled ? 1 : 0 source = "git::https://github.com/canonical/istio-ingress-k8s-operator//terraform" - app_name = var.catalogue.app_name - channel = local.channels.catalogue - config = var.catalogue.config - constraints = var.catalogue.constraints + app_name = var.istio_ingress.app_name + channel = local.channels.istio_ingress + config = var.istio_ingress.config + constraints = var.istio_ingress.constraints model_uuid = var.model_uuid - revision = local.revisions.catalogue - storage_directives = var.catalogue.storage_directives - units = var.catalogue.units + revision = local.revisions.istio_ingress + storage_directives = var.istio_ingress.storage_directives + units = var.istio_ingress.units } -module "istio-beacon" { +module "istio_beacon" { count = var.mesh.enabled ? 1 : 0 source = "git::https://github.com/canonical/istio-beacon-k8s-operator//terraform" - app_name = var.catalogue.app_name - channel = local.channels.catalogue - config = var.catalogue.config - constraints = var.catalogue.constraints + app_name = var.istio_beacon.app_name + channel = local.channels.istio_beacon + config = var.istio_beacon.config + constraints = var.istio_beacon.constraints model_uuid = var.model_uuid - revision = local.revisions.catalogue - storage_directives = var.catalogue.storage_directives - units = var.catalogue.units + revision = local.revisions.istio_beacon + storage_directives = var.istio_beacon.storage_directives + units = var.istio_beacon.units } module "loki_coordinator" { diff --git a/terraform/cos-dev/integrations.tf b/terraform/cos-dev/integrations.tf index 82f9b721..5aa32815 100644 --- a/terraform/cos-dev/integrations.tf +++ b/terraform/cos-dev/integrations.tf @@ -655,8 +655,8 @@ resource "juju_integration" "ingress" { model_uuid = var.model_uuid application { - name = module.traefik.app_name - endpoint = module.traefik.endpoints.ingress + name = module.traefik[0].app_name + endpoint = module.traefik[0].endpoints.ingress } application { @@ -676,8 +676,8 @@ resource "juju_integration" "grafana_ingress" { } application { - name = module.traefik.app_name - endpoint = module.traefik.endpoints.ingress + name = module.traefik[0].app_name + endpoint = module.traefik[0].endpoints.ingress } lifecycle { replace_triggered_by = [terraform_data.grafana_ingress_interface, terraform_data.grafana_litestream_resource] } @@ -700,8 +700,8 @@ resource "juju_integration" "traefik_route" { model_uuid = var.model_uuid application { - name = module.traefik.app_name - endpoint = module.traefik.endpoints.traefik_route + name = module.traefik[0].app_name + endpoint = module.traefik[0].endpoints.traefik_route } application { @@ -863,8 +863,8 @@ resource "juju_integration" "traefik_receive_ca_certificate" { } application { - name = module.traefik.app_name - endpoint = module.traefik.endpoints.receive_ca_cert + name = module.traefik[0].app_name + endpoint = module.traefik[0].endpoints.receive_ca_cert } } @@ -875,12 +875,12 @@ resource "juju_integration" "external_traefik_certificates" { model_uuid = var.model_uuid application { - offer_url = var.external_certificates_offer_url + offer_url = var.reverse_proxy.cmr_urls.certificates } application { - name = module.traefik.app_name - endpoint = module.traefik.endpoints.certificates + name = module.traefik[0].app_name + endpoint = module.traefik[0].endpoints.certificates } } @@ -889,7 +889,7 @@ resource "juju_integration" "external_grafana_ca_cert" { model_uuid = var.model_uuid application { - offer_url = var.external_ca_cert_offer_url + offer_url = var.reverse_proxy.cmr_urls.receive_ca_cert } application { @@ -903,7 +903,7 @@ resource "juju_integration" "external_otelcol_ca_cert" { model_uuid = var.model_uuid application { - offer_url = var.external_ca_cert_offer_url + offer_url = var.reverse_proxy.cmr_urls.receive_ca_cert } application { diff --git a/terraform/cos-dev/locals.tf b/terraform/cos-dev/locals.tf index b821a6a0..14f75b58 100644 --- a/terraform/cos-dev/locals.tf +++ b/terraform/cos-dev/locals.tf @@ -2,13 +2,15 @@ locals { reverse_proxy_termination = var.reverse_proxy.enabled && var.reverse_proxy.cmr_urls.certificates != null ? true : false traefik_base = "ubuntu@20.04" tracks = { - alertmanager = "dev" - catalogue = "dev" - grafana = "dev" - loki = "dev" - mimir = "dev" - otelcol = "dev" - tempo = "dev" + alertmanager = "dev" + catalogue = "dev" + grafana = "dev" + istio_beacon = "dev" + istio_ingress = "dev" + loki = "dev" + mimir = "dev" + otelcol = "dev" + tempo = "dev" s3_integrator = "2" seaweedfs = "latest" @@ -19,6 +21,8 @@ locals { alertmanager = "${local.tracks.alertmanager}/${var.risk}" catalogue = "${local.tracks.catalogue}/${var.risk}" grafana = "${local.tracks.grafana}/${var.risk}" + istio_beacon = "${local.tracks.istio_beacon}/${var.risk}" + istio_ingress = "${local.tracks.istio_ingress}/${var.risk}" loki = "${local.tracks.loki}/${var.risk}" mimir = "${local.tracks.mimir}/${var.risk}" otelcol = "${local.tracks.otelcol}/${var.risk}" @@ -32,6 +36,8 @@ locals { alertmanager = var.alertmanager.revision != null ? var.alertmanager.revision : data.juju_charm.alertmanager_info.revision catalogue = var.catalogue.revision != null ? var.catalogue.revision : data.juju_charm.catalogue_info.revision grafana = var.grafana.revision != null ? var.grafana.revision : data.juju_charm.grafana_info.revision + istio_beacon = var.istio_beacon.revision != null ? var.istio_beacon.revision : data.juju_charm.istio_beacon_info.revision + istio_ingress = var.istio_ingress.revision != null ? var.istio_ingress.revision : data.juju_charm.istio_ingress_info.revision loki_coordinator = var.loki_coordinator.revision != null ? var.loki_coordinator.revision : data.juju_charm.loki_coordinator_info.revision loki_worker = var.loki_worker.revision != null ? var.loki_worker.revision : data.juju_charm.loki_worker_info.revision mimir_coordinator = var.mimir_coordinator.revision != null ? var.mimir_coordinator.revision : data.juju_charm.mimir_coordinator_info.revision diff --git a/terraform/cos-dev/outputs.tf b/terraform/cos-dev/outputs.tf index 733e3bdc..6de7ddc5 100644 --- a/terraform/cos-dev/outputs.tf +++ b/terraform/cos-dev/outputs.tf @@ -34,8 +34,8 @@ output "components" { alertmanager = module.alertmanager catalogue = module.catalogue grafana = module.grafana - grafana = try(module.istio_beacon[0], null) - grafana = try(module.istio_ingress[0], null) + istio_beacon = try(module.istio_beacon[0], null) + istio_ingress = try(module.istio_ingress[0], null) loki_coordinator = module.loki_coordinator loki_worker = try(module.loki_worker[0], null) loki_worker_backend = try(module.loki_worker_backend[0], null) diff --git a/terraform/cos-dev/tests/mesh.tftest.hcl b/terraform/cos-dev/tests/mesh.tftest.hcl index c3ff57eb..2ceba568 100644 --- a/terraform/cos-dev/tests/mesh.tftest.hcl +++ b/terraform/cos-dev/tests/mesh.tftest.hcl @@ -10,7 +10,7 @@ run "mesh_and_reverse_proxy_enabled_fails" { command = plan variables { - mesh = { enabled = false } + mesh = { enabled = true } reverse_proxy = { enabled = true } } @@ -34,13 +34,13 @@ run "mesh_disabled" { command = plan assert { - condition = length(module.istio-beacon) == 0 - error_message = "Expected no istio-beacon module when the mesh is disabled" + condition = length(module.istio_beacon) == 0 + error_message = "Expected no istio_beacon module when the mesh is disabled" } assert { - condition = length(module.istio-ingress) == 0 - error_message = "Expected no istio-ingress module when the mesh is disabled" + condition = length(module.istio_ingress) == 0 + error_message = "Expected no istio_ingress module when the mesh is disabled" } assert { @@ -70,13 +70,13 @@ run "mesh_enabled" { } assert { - condition = length(module.istio-beacon) == 1 - error_message = "Expected istio-beacon module when the mesh is enabled" + condition = length(module.istio_beacon) == 1 + error_message = "Expected istio_beacon module when the mesh is enabled" } assert { - condition = length(module.istio-ingress) == 1 - error_message = "Expected istio-ingress module when the mesh is enabled" + condition = length(module.istio_ingress) == 1 + error_message = "Expected istio_ingress module when the mesh is enabled" } assert { diff --git a/terraform/cos-dev/tests/reverse_proxy.tftest.hcl b/terraform/cos-dev/tests/reverse_proxy.tftest.hcl index 8e105460..53058b8c 100644 --- a/terraform/cos-dev/tests/reverse_proxy.tftest.hcl +++ b/terraform/cos-dev/tests/reverse_proxy.tftest.hcl @@ -10,7 +10,7 @@ run "reverse_proxy_enabled" { command = plan assert { - condition = length(module.traefik) == 0 + condition = length(module.traefik) == 1 error_message = "Expected traefik module when the reverse proxy is enabled" } diff --git a/terraform/cos-dev/upgrades.tf b/terraform/cos-dev/upgrades.tf index 270c68f8..90c733ae 100644 --- a/terraform/cos-dev/upgrades.tf +++ b/terraform/cos-dev/upgrades.tf @@ -38,6 +38,18 @@ data "juju_charm" "grafana_info" { base = var.base } +data "juju_charm" "istio_beacon_info" { + charm = "istio-beacon-k8s" + channel = local.channels.istio_beacon + base = var.base +} + +data "juju_charm" "istio_ingress_info" { + charm = "istio-ingress-k8s" + channel = local.channels.istio_ingress + base = var.base +} + data "juju_charm" "loki_coordinator_info" { charm = "loki-coordinator-k8s" channel = local.channels.loki From 04c22bc007033ff47b8b1d9027f9179e44bb4cd4 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Tue, 19 May 2026 12:09:04 -0400 Subject: [PATCH 04/11] chore --- terraform/cos-dev/README.md | 13 +++++++------ terraform/cos-dev/tests/reverse_proxy.tftest.hcl | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/terraform/cos-dev/README.md b/terraform/cos-dev/README.md index fcb03a75..9ff8b7be 100644 --- a/terraform/cos-dev/README.md +++ b/terraform/cos-dev/README.md @@ -19,6 +19,8 @@ This module is intended for development and testing environments where full HA i | [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 | +| [istio\_beacon](#module\_istio\_beacon) | git::https://github.com/canonical/istio-beacon-k8s-operator//terraform | n/a | +| [istio\_ingress](#module\_istio\_ingress) | git::https://github.com/canonical/istio-ingress-k8s-operator//terraform | n/a | | [loki\_coordinator](#module\_loki\_coordinator) | git::https://github.com/canonical/loki-operators//coordinator/terraform | n/a | | [loki\_worker](#module\_loki\_worker) | git::https://github.com/canonical/loki-operators//worker/terraform | n/a | | [loki\_worker\_backend](#module\_loki\_worker\_backend) | git::https://github.com/canonical/loki-operators//worker/terraform | n/a | @@ -49,28 +51,27 @@ This module is intended for development and testing environments where full HA i | [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 | | [base](#input\_base) | The operating system on which to deploy. E.g. ubuntu@24.04. Check Charmhub for per-charm base support. | `string` | `"ubuntu@24.04"` | 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 | -| [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 | | [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 | | [ingress](#input\_ingress) | Per-component toggle for ingress integrations |
object({
alertmanager = optional(bool, true)
catalogue = optional(bool, true)
grafana = optional(bool, true)
loki = optional(bool, true)
mimir = optional(bool, true)
opentelemetry_collector = optional(bool, true)
tempo = optional(bool, true)
})
| `{}` | 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\_bucket](#input\_loki\_bucket) | Loki S3 bucket name | `string` | `"loki"` | no | +| [istio\_beacon](#input\_istio\_beacon) | Application configuration for istio-beacon. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application |
object({
app_name = optional(string, "istio-beacon")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | +| [istio\_ingress](#input\_istio\_ingress) | Application configuration for istio-ingress. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application |
object({
app_name = optional(string, "istio-ingress")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | | [loki\_coordinator](#input\_loki\_coordinator) | Application configuration for the Loki coordinator. 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 | | [loki\_worker](#input\_loki\_worker) | Application configuration for the Loki worker(s). For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application |
object({
app_name = optional(string, "loki-worker")
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
# Monolithic mode (role-all)
config = optional(map(string), {})
storage_directives = optional(map(string), {})
units = optional(number, 1)
# Distributed mode
backend_config = optional(map(string), {})
read_config = optional(map(string), {})
write_config = optional(map(string), {})
backend_storage_directives = optional(map(string), {})
read_storage_directives = optional(map(string), {})
write_storage_directives = optional(map(string), {})
backend_units = optional(number, 1)
read_units = optional(number, 1)
write_units = optional(number, 1)
})
| `{}` | no | -| [mimir\_bucket](#input\_mimir\_bucket) | Mimir S3 bucket name | `string` | `"mimir"` | no | +| [mesh](#input\_mesh) | Configure the service mesh. |
object({
enabled = optional(bool, false)
cmr_urls = optional(object({
alermanager = optional(string, null)
catalogue = optional(string, null)
grafana = optional(string, null)
loki = optional(string, null)
mimir = optional(string, null)
tempo = optional(string, null)
}), {}
)
})
| `{}` | no | | [mimir\_coordinator](#input\_mimir\_coordinator) | Application configuration for the Mimir coordinator. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application |
object({
app_name = optional(string, "mimir")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | | [mimir\_worker](#input\_mimir\_worker) | Application configuration for the Mimir worker(s). For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application |
object({
app_name = optional(string, "mimir-worker")
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
# Monolithic mode (role-all)
config = optional(map(string), {})
storage_directives = optional(map(string), {})
units = optional(number, 1)
# Distributed mode
backend_config = optional(map(string), {})
read_config = optional(map(string), {})
write_config = optional(map(string), {})
backend_storage_directives = optional(map(string), {})
read_storage_directives = optional(map(string), {})
write_storage_directives = optional(map(string), {})
backend_units = optional(number, 1)
read_units = optional(number, 1)
write_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 | | [opentelemetry\_collector](#input\_opentelemetry\_collector) | Application configuration for OpenTelemetry Collector. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application |
object({
app_name = optional(string, "otelcol")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | +| [reverse\_proxy](#input\_reverse\_proxy) | Configure the reverse proxy. |
object({
enabled = optional(bool, true)
cmr_urls = optional(object({
certificates = optional(string, null)
receive_ca_cert = optional(string, null)
}), {}
)
})
| `{}` | no | | [risk](#input\_risk) | Risk level that the applications are (unless overwritten by individual channels) deployed from | `string` | `"edge"` | no | | [s3\_access\_key](#input\_s3\_access\_key) | S3 access-key credential. Required when storage\_backend is 's3'. | `string` | `null` | no | +| [s3\_buckets](#input\_s3\_buckets) | S3 bucket names for components |
object({
loki = optional(string, "loki")
mimir = optional(string, "mimir")
tempo = optional(string, "tempo")
})
| `{}` | no | | [s3\_endpoint](#input\_s3\_endpoint) | S3 endpoint URL. Required when storage\_backend is 's3'. | `string` | `null` | no | | [s3\_integrator](#input\_s3\_integrator) | Application configuration shared by all S3-integrators (one deployed per coordinated worker). 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, 1)
})
| `{}` | no | | [s3\_secret\_key](#input\_s3\_secret\_key) | S3 secret-key credential. Required when storage\_backend is 's3'. | `string` | `null` | no | | [seaweedfs](#input\_seaweedfs) | Application configuration for SeaweedFS. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application |
object({
app_name = optional(string, "seaweedfs")
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")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | | [storage\_backend](#input\_storage\_backend) | Storage backend: 'seaweedfs' (built-in S3-compatible storage) or 's3' (external S3/Ceph via s3-integrator). | `string` | `"seaweedfs"` | no | -| [tempo\_bucket](#input\_tempo\_bucket) | Tempo S3 bucket name | `string` | `"tempo"` | no | | [tempo\_coordinator](#input\_tempo\_coordinator) | Application configuration for the Tempo coordinator. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application |
object({
app_name = optional(string, "tempo")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | | [tempo\_worker](#input\_tempo\_worker) | Application configuration for the Tempo worker(s). For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application |
object({
app_name = optional(string, "tempo-worker")
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
# Monolithic mode (role-all)
config = optional(map(string), {})
storage_directives = optional(map(string), {})
units = optional(number, 1)
# Distributed mode
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), {})
querier_storage_directives = optional(map(string), {})
query_frontend_storage_directives = optional(map(string), {})
ingester_storage_directives = optional(map(string), {})
distributor_storage_directives = optional(map(string), {})
compactor_storage_directives = optional(map(string), {})
metrics_generator_storage_directives = optional(map(string), {})
querier_units = optional(number, 1)
query_frontend_units = optional(number, 1)
ingester_units = optional(number, 1)
distributor_units = optional(number, 1)
compactor_units = optional(number, 1)
metrics_generator_units = optional(number, 1)
})
| `{}` | no | | [topology](#input\_topology) | Deployment topology: 'monolithic' (single role-all worker per component) or 'distributed' (separate workers per role). | `string` | `"monolithic"` | no | diff --git a/terraform/cos-dev/tests/reverse_proxy.tftest.hcl b/terraform/cos-dev/tests/reverse_proxy.tftest.hcl index 53058b8c..357fd464 100644 --- a/terraform/cos-dev/tests/reverse_proxy.tftest.hcl +++ b/terraform/cos-dev/tests/reverse_proxy.tftest.hcl @@ -8,7 +8,7 @@ variables { run "reverse_proxy_enabled" { command = plan - + assert { condition = length(module.traefik) == 1 error_message = "Expected traefik module when the reverse proxy is enabled" @@ -40,7 +40,7 @@ run "reverse_proxy_disabled" { mesh = { enabled = false } reverse_proxy = { enabled = false } } - + assert { condition = length(module.traefik) == 0 error_message = "Expected no traefik module when the reverse proxy is disabled" From 50e0ce6dddac4b4981d09763f7bef33fa67d8b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Mass=C3=B3n?= <939888+Abuelodelanada@users.noreply.github.com> Date: Fri, 15 May 2026 05:05:23 -0300 Subject: [PATCH 05/11] fix: Grafana agent logo replaced with otelcol logo (#345) --- docs/assets/telemetry-flow-1.jpeg | Bin 78368 -> 45148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/assets/telemetry-flow-1.jpeg b/docs/assets/telemetry-flow-1.jpeg index dcf0608860027a4d667aa1a7ce10b28e63e7f9f9..9089216b3c2df4308249d205e7b6fba5aa331dde 100644 GIT binary patch literal 45148 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
MP)I@y?lPZ$Hf0q?vG1_KI@28U7&+}uhw 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 literal 78368 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 IfP5YKFZAHnQvd(} From 40e8852faf7ee1de2bcaf4d0ff15df4a24c0d075 Mon Sep 17 00:00:00 2001 From: Leon <82407168+sed-i@users.noreply.github.com> Date: Fri, 15 May 2026 10:24:03 +0200 Subject: [PATCH 06/11] feat(doc): add install guide (#289) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michael Thamm Co-authored-by: Sina P <55766091+sinapah@users.noreply.github.com> Co-authored-by: Jose Massón <939888+Abuelodelanada@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/prompts/review-docs.prompt.md | 1 + .../evaluate-telemetry-volume.md | 24 +++- docs/how-to/deploy-and-manage/index.md | 17 ++- docs/how-to/deploy-and-manage/install.md | 106 ++++++++++++++++++ docs/reference/glossary.md | 9 +- 5 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 docs/how-to/deploy-and-manage/install.md diff --git a/.github/prompts/review-docs.prompt.md b/.github/prompts/review-docs.prompt.md index be37ec32..7f9f49f5 100644 --- a/.github/prompts/review-docs.prompt.md +++ b/.github/prompts/review-docs.prompt.md @@ -13,6 +13,7 @@ Review the documentation for clarity, completeness, and accuracy. - H1 titles under `docs/how-to` should start with "How to". - Section headers and index entries should all use sentence case (not title case). - Known product names should be capitalized consistently throughout the documentation. +- Spelling according to US English conventions. - Changes in docs should be reflected in the glossary, if the changes are related to terms/concepts that are unique to COS or charmed observability (don't redefine all/general terms). diff --git a/docs/how-to/configure-and-tune/evaluate-telemetry-volume.md b/docs/how-to/configure-and-tune/evaluate-telemetry-volume.md index cb657f97..3e322b4c 100644 --- a/docs/how-to/configure-and-tune/evaluate-telemetry-volume.md +++ b/docs/how-to/configure-and-tune/evaluate-telemetry-volume.md @@ -11,12 +11,27 @@ In order to correctly size the VM(s) needed for COS, you need to know how much t ## Metrics rate + +### Manual evaluation +Find out the metrics endpoint manifest for each observed workload. If it is not documented, +you will need to manually count the number of non-comment lines served on the metrics endpoint, +for example: + +```bash +curl -sf localhost:8080/metrics | grep -v "^# " | wc -l +``` + +This will give you the number of time series that will be created for the workload, per unit. + +Another option is to deploy a temporary pilot Prometheus charm. + +### With charmed Prometheus Have your deployment sending all metrics to Prometheus (or Mimir) and inspect the 48hr plot for `count({__name__=~".+"})`. The raw data can also be obtained by querying the Prometheus `query` endpoint directly: ```bash curl -s --data-urlencode 'query=count({__name__=~".+"})' \ - ${PROM_UNIT_IP}:9090/api/v1/query" + http://${PROM_UNIT_IP}:9090/api/v1/query ``` Compare the output to the number of metrics exposed by individual applications. @@ -36,8 +51,13 @@ load[load generator] ---|db| postgresql postgresql ---|metrics-endpoint| prometheus ``` - ## Logs rate +### Manual evaluation +The most reliable way to evaluate the logging rate of a workload is with load tests. + +Another option is to deploy temporary pilot Loki and Prometheus charms. + +### With charmed Loki and Prometheus Have your deployment sending all logs to Loki, and inspect the 48hr plot for `loki_distributor_*_received_total`: ``` diff --git a/docs/how-to/deploy-and-manage/index.md b/docs/how-to/deploy-and-manage/index.md index 339dbbda..0777d304 100644 --- a/docs/how-to/deploy-and-manage/index.md +++ b/docs/how-to/deploy-and-manage/index.md @@ -14,14 +14,10 @@ These guides cover deploying, upgrading, managing, and securing access to COS. See our [tutorials](/tutorial/index) for guidance on deploying COS. -## Upgrades - -Move between COS revisions with confidence. - ```{toctree} :maxdepth: 1 -Cross-track upgrade instructions +Install ``` ## Secure access @@ -34,3 +30,14 @@ Protect and expose COS endpoints for production traffic. Configure TLS encryption Configure ingress ``` + +## Upgrades + +Move between COS revisions with confidence. + +```{toctree} +:maxdepth: 1 + +Cross-track upgrade instructions +``` + diff --git a/docs/how-to/deploy-and-manage/install.md b/docs/how-to/deploy-and-manage/install.md new file mode 100644 index 00000000..dd03d75b --- /dev/null +++ b/docs/how-to/deploy-and-manage/install.md @@ -0,0 +1,106 @@ +--- +myst: + html_meta: + description: "Install the Canonical Observability Stack: preparation checklist covering sizing, networking, storage, and deployment options." +--- + +# How to install COS + +This guide walks you through the prerequisites and deployment planning needed to install the Canonical Observability Stack (COS). Use it to prepare your environment, create a Terraform plan, and deploy supporting components in the recommended order. + +## Preparation + +Before deploying COS or COS Lite, work through the items below. + +### COS flavor + +The [flavor of COS](/explanation/overview/what-is-cos) to install depends on your use case. +If you want to install on edge devices, want to rely on local storage, or do not need high availability, then COS Lite is likely the right choice; otherwise +you should probably go with "full" COS. + +```{mermaid} +graph LR + +subgraph env["Monitored environment"] +opentelemetry-collector +end + +subgraph k8s["K8s cluster"] +COS +end + +subgraph pc["Public cloud"] +cos-alerter["COS Alerter"] +end + +subgraph storage["Storage cluster"] +S3 +end + +opentelemetry-collector ---|telemetry| COS +COS --- S3 +COS --- cos-alerter +``` + +### Kubernetes cluster + +Deploy COS on a high-availability Kubernetes cluster with at least 3 control plane nodes. + +### Sizing + +Use the [sizing guide](/reference/system-requirements) to determine the minimum hardware for your deployment. +If you don't yet know how much telemetry your workloads generate, start with [How to evaluate telemetry volume](/how-to/configure-and-tune/evaluate-telemetry-volume). + +Follow the [storage best practices](/reference/storage) to set up a distributed storage backend with a replication factor of 3. +Do **not** use `hostPath` storage in production. + +### Configure networking + +Review the [networking best practices](/reference/networking) and ensure: + +- A load balancer (for example, MetalLB) is available to give Traefik a stable IP. +- Egress is open for Charmhub, the Juju OCI registry, and Snapcraft. + +### Plan for TLS + +Production deployments should use TLS. +See [How to configure TLS encryption](/how-to/deploy-and-manage/configure-tls-encryption) for the available modes and what you need to prepare (for example, an external certificates provider). + +### Authentication and authorization +Only the Grafana and Traefik charms support authentication. +To expose Grafana publicly, deploy two Traefik charms: one for internal connections and another for external access to provide ingress. + +### Dedicated Juju controller and model + +You should bootstrap a dedicated Juju controller and model just for COS. + +## Terraform plan + +Create a `main.tf` file like this one: + +```hcl +resource "juju_model" "cos" { + name = "cos" +} + +module "cos" { + source = "git::https://github.com/canonical/observability-stack//terraform/cos?ref=tf-cos-3.0.n" + risk = "stable" + model_uuid = juju_model.cos.uuid + s3_endpoint = "http://IP_ADDRESS:PORT" + s3_secret_key = "REPLACE_ME" + s3_access_key = "REPLACE_ME" +} +``` + +### Revision pins +Revision pinning is optional. With revision pins, subsequent `terraform apply` invocation will not refresh charms. Without revision pins, each `terraform apply` would refresh to the latest revision in track, if a new one released. +where `.n` in `tf-cos-3.0.n` is the latest available patch version in the [COS tags](https://github.com/canonical/observability-stack/tags) list. + +### Revision pins + +Deploying COS without revision pins, per component, will deploy the latest charms revisions in-track. Any subsequent Terraform plans will experience the same behaviour i.e., keeping COS up-to-date. However, if you require more stability, it is advised to pin the charm revisions of all components. + +## Deploy COS Alerter + +COS Alerter is a watchdog service for COS. Deploy it on dedicated infrastructure that is separate from your COS or COS Lite deployment. For more information, including deployment details, see the [COS Alerter repository](https://github.com/canonical/cos-alerter). \ No newline at end of file diff --git a/docs/reference/glossary.md b/docs/reference/glossary.md index feac61d0..6bb60036 100644 --- a/docs/reference/glossary.md +++ b/docs/reference/glossary.md @@ -35,7 +35,7 @@ alerting pipeline has failed. See [Topology](/reference/topology). ## COS Configuration -A [peripheral charm](#peripheral-charm) that clones a git repository (via a `git-sync` +A peripheral charm that clones a git repository (via a `git-sync` workload) and provisions alert rules, dashboards, and scrape targets from it into COS. Enables git-ops–style configuration independently of any charmed operator. See [COS components](/reference/cos-components). @@ -90,10 +90,9 @@ per-charm host-health alerts. See [Generic alert rule groups](/explanation/alert ## Git-ops alert rules -Alert rules (and dashboards or scrape targets) loaded from an external git repository via -the [COS Configuration](#cos-configuration) charm. Allows version-controlled, -operator-defined rules outside of any charmed operator. -See [Alert rules](/explanation/alerting/alerting-overview). +Alert rules (and dashboards or scrape targets) loaded from an external git repository via the COS +Configuration charm. Allows version-controlled, operator-defined rules outside of any charmed +operator. See [Alert rules](/explanation/alerting/alerting-overview). ## Juju topology labels From 130db544e0908980ed60a816488e6accec1cd6ec Mon Sep 17 00:00:00 2001 From: Leon <82407168+sed-i@users.noreply.github.com> Date: Fri, 15 May 2026 10:33:30 +0200 Subject: [PATCH 07/11] chore: update cos-lite tutorial (#323) Co-authored-by: Michael Thamm Co-authored-by: Sina P <55766091+sinapah@users.noreply.github.com> --- docs/tutorial/cos-canonical-k8s-sandbox.conf | 48 +--- docs/tutorial/cos-canonical-k8s-sandbox.md | 264 +++++++++++------- .../cos-lite-canonical-k8s-sandbox.conf | 34 --- .../cos-lite-canonical-k8s-sandbox.md | 168 +++++------ docs/tutorial/cos-lite-microk8s-sandbox.md | 14 +- docs/tutorial/cos-lite-microk8s-sandbox.tf | 22 -- terraform/cos-lite/README.md | 23 +- 7 files changed, 281 insertions(+), 292 deletions(-) delete mode 100644 docs/tutorial/cos-lite-canonical-k8s-sandbox.conf delete mode 100644 docs/tutorial/cos-lite-microk8s-sandbox.tf diff --git a/docs/tutorial/cos-canonical-k8s-sandbox.conf b/docs/tutorial/cos-canonical-k8s-sandbox.conf index 2e919987..b71b9612 100644 --- a/docs/tutorial/cos-canonical-k8s-sandbox.conf +++ b/docs/tutorial/cos-canonical-k8s-sandbox.conf @@ -15,6 +15,12 @@ snap: - snap install k8s --classic --channel=1.33-classic/stable - snap install terraform --classic +write_files: +- source: + uri: https://github.com/canonical/observability-stack/raw/refs/heads/main/tests/integration/cos/tls_internal/track-dev.tf + path: /var/run/cos/cos-demo.tf + permissions: '0755' + runcmd: - | # [docs:setup-s3] @@ -55,45 +61,17 @@ runcmd: # Set up terraform plan for COS - # [docs:create-terraform-module] - sudo -u ubuntu mkdir ~ubuntu/cos - sudo -u ubuntu tee ~ubuntu/cos/cos-demo.tf << EOF - terraform { - required_version = ">= 1.5" - required_providers { - juju = { - source = "juju/juju" - version = "~> 1.0" - } - } - } - - resource "juju_model" "cos" { - name = "cos" - config = { logging-config = "=WARNING; unit=DEBUG" } - } - - module "cos" { - source = "git::https://github.com/canonical/observability-stack//terraform/cos?ref=main" - model_uuid = juju_model.cos.uuid - risk = "edge" - anti_affinity = false - internal_tls = false - external_certificates_offer_url = null - s3_endpoint = "http://$IPADDR:8080" - s3_secret_key = "secret-key" - s3_access_key = "access-key" - } - EOF - # [docs:create-terraform-module-end] - echo "Deploying COS..." + sudo -u ubuntu juju add-model cos + sudo -u ubuntu mkdir -p ~ubuntu/cos + sudo -u ubuntu cp /var/run/cos/cos-demo.tf ~ubuntu/cos + sudo -u ubuntu terraform -chdir=$(echo ~ubuntu/cos) init - sudo -u ubuntu terraform -chdir=$(echo ~ubuntu/cos) apply -auto-approve + sudo -u ubuntu terraform -chdir=$(echo ~ubuntu/cos) apply -auto-approve -var="model=cos" -var="s3_endpoint=http://$IPADDR:8080" -var="s3_secret_key=secret-key" -var="s3_access_key=access-key" # The first "terraform apply" might fail due to https://github.com/juju/terraform-provider-juju/issues/767. # Running apply again resolves this. - sudo -u ubuntu terraform -chdir=$(echo ~ubuntu/cos) apply -auto-approve + sudo -u ubuntu terraform -chdir=$(echo ~ubuntu/cos) apply -auto-approve -var="model=cos" -var="s3_endpoint=http://$IPADDR:8080" -var="s3_secret_key=secret-key" -var="s3_access_key=access-key" echo "Waiting for model to settle..." sudo -u ubuntu juju wait-for model cos --query='forEach(units, unit => unit.agent-status == "idle") && forEach(applications, app => app.status == "active")' --timeout=10m -final_message: "The COS Lite appliance is ready, after $UPTIME seconds" +final_message: "The COS appliance is ready, after $UPTIME seconds" \ No newline at end of file diff --git a/docs/tutorial/cos-canonical-k8s-sandbox.md b/docs/tutorial/cos-canonical-k8s-sandbox.md index e688bd28..95175c25 100644 --- a/docs/tutorial/cos-canonical-k8s-sandbox.md +++ b/docs/tutorial/cos-canonical-k8s-sandbox.md @@ -36,13 +36,10 @@ and configure RadosGW to listen on port 8080 ([doc](https://canonical-microceph. ## Deploy COS using Terraform -Assuming you are using the username `ubuntu`, create a `cos-demo.tf` file as follows: +Create a `cos-demo.tf` file as follows: -```{literalinclude} /tutorial/cos-canonical-k8s-sandbox.conf - :language: bash - :start-after: [docs:create-terraform-module] - :end-before: [docs:create-terraform-module-end] - :dedent: 4 +```{literalinclude} /../tests/integration/cos/tls_internal/track-dev.tf + :language: hcl ``` **Note**: You can customize further the number of units of each distributed charm and other aspects of COS: have a look at the [`variables.tf`](../../terraform/cos/variables.tf) file of the COS Terraform module for the complete documentation. @@ -50,11 +47,14 @@ Assuming you are using the username `ubuntu`, create a `cos-demo.tf` file as fol To deploy COS in a new model named `cos`, run: ```bash -$ terraform init -$ terraform apply +terraform init +terraform apply -var="model=cos" \ + -var="s3_endpoint=$IPADDR" \ + -var="s3_secret_key=secret-key" \ + -var="s3_access_key=access-key" ``` -You can watch the model as it settles with: +You can watch the model as it settles: ``` juju status --model cos --relations --watch=5s @@ -66,91 +66,163 @@ The output of `juju status --relations` for your deployment should eventually be ``` Model Controller Cloud/Region Version SLA Timestamp -cos ck8s ck8s 3.6.7 unsupported 10:55:15-00:00 - -App Version Status Scale Charm Channel Rev Address Exposed Message -alertmanager 0.27.0 active 1 alertmanager-k8s 2/edge 171 10.152.183.180 no -catalogue active 1 catalogue-k8s 2/edge 94 10.152.183.145 no -grafana 9.5.3 active 1 grafana-k8s 2/edge 155 10.152.183.144 no -grafana-agent 0.40.4 active 1 grafana-agent-k8s 2/edge 148 10.152.183.251 no grafana-dashboards-provider: off -loki active 3 loki-coordinator-k8s 2/edge 35 10.152.183.128 no -loki-backend 3.0.0 active 3 loki-worker-k8s 2/edge 48 10.152.183.22 no backend ready. -loki-read 3.0.0 active 3 loki-worker-k8s 2/edge 48 10.152.183.161 no read ready. -loki-s3-integrator active 1 s3-integrator 2/edge 157 10.152.183.159 no -loki-write 3.0.0 active 3 loki-worker-k8s 2/edge 48 10.152.183.188 no write ready. -mimir active 3 mimir-coordinator-k8s 2/edge 57 10.152.183.187 no -mimir-backend 2.13.0 active 3 mimir-worker-k8s 2/edge 55 10.152.183.168 no backend ready. -mimir-read 2.13.0 active 3 mimir-worker-k8s 2/edge 55 10.152.183.129 no read ready. -mimir-s3-integrator active 1 s3-integrator 2/edge 157 10.152.183.184 no -mimir-write 2.13.0 active 3 mimir-worker-k8s 2/edge 55 10.152.183.225 no write ready. -tempo active 3 tempo-coordinator-k8s 2/edge 91 10.152.183.123 no -tempo-compactor 2.7.1 active 3 tempo-worker-k8s 2/edge 66 10.152.183.138 no compactor ready. -tempo-distributor 2.7.1 active 3 tempo-worker-k8s 2/edge 66 10.152.183.27 no distributor ready. -tempo-ingester 2.7.1 active 3 tempo-worker-k8s 2/edge 66 10.152.183.91 no ingester ready. -tempo-metrics-generator 2.7.1 active 3 tempo-worker-k8s 2/edge 66 10.152.183.200 no metrics-generator ready. -tempo-querier 2.7.1 active 3 tempo-worker-k8s 2/edge 66 10.152.183.59 no querier ready. -tempo-query-frontend 2.7.1 active 3 tempo-worker-k8s 2/edge 66 10.152.183.109 no query-frontend ready. -tempo-s3-integrator active 1 s3-integrator 2/edge 157 10.152.183.205 no -traefik 2.11.0 active 1 traefik-k8s latest/stable 236 10.152.183.197 no Serving at 10.63.93.138 - -Unit Workload Agent Address Ports Message -alertmanager/0* active idle 10.1.0.38 -catalogue/0* active idle 10.1.0.2 -grafana-agent/0* active idle 10.1.0.242 grafana-dashboards-provider: off -grafana/0* active idle 10.1.0.108 -loki-backend/0 active idle 10.1.0.154 backend ready. -loki-backend/1 active idle 10.1.0.162 backend ready. -loki-backend/2* active idle 10.1.0.140 backend ready. -loki-read/0* active idle 10.1.0.187 read ready. -loki-read/1 active idle 10.1.0.169 read ready. -loki-read/2 active idle 10.1.0.235 read ready. -loki-s3-integrator/0* active idle 10.1.0.72 -loki-write/0* active idle 10.1.0.115 write ready. -loki-write/1 active idle 10.1.0.239 write ready. -loki-write/2 active idle 10.1.0.182 write ready. -loki/0 active idle 10.1.0.35 -loki/1* active idle 10.1.0.90 -loki/2 active idle 10.1.0.10 -mimir-backend/0* active idle 10.1.0.3 backend ready. -mimir-backend/1 active idle 10.1.0.24 backend ready. -mimir-backend/2 active idle 10.1.0.147 backend ready. -mimir-read/0* active idle 10.1.0.103 read ready. -mimir-read/1 active idle 10.1.0.112 read ready. -mimir-read/2 active idle 10.1.0.158 read ready. -mimir-s3-integrator/0* active idle 10.1.0.171 -mimir-write/0* active idle 10.1.0.160 write ready. -mimir-write/1 active idle 10.1.0.155 write ready. -mimir-write/2 active idle 10.1.0.96 write ready. -mimir/0 active idle 10.1.0.117 -mimir/1 active idle 10.1.0.222 -mimir/2* active idle 10.1.0.134 -tempo-compactor/0* active idle 10.1.0.204 compactor ready. -tempo-compactor/1 active idle 10.1.0.191 compactor ready. -tempo-compactor/2 active idle 10.1.0.181 compactor ready. -tempo-distributor/0 active idle 10.1.0.53 distributor ready. -tempo-distributor/1 active idle 10.1.0.176 distributor ready. -tempo-distributor/2* active idle 10.1.0.184 distributor ready. -tempo-ingester/0 active idle 10.1.0.221 ingester ready. -tempo-ingester/1 active idle 10.1.0.78 ingester ready. -tempo-ingester/2* active idle 10.1.0.109 ingester ready. -tempo-metrics-generator/0 active idle 10.1.0.13 metrics-generator ready. -tempo-metrics-generator/1* active idle 10.1.0.40 metrics-generator ready. -tempo-metrics-generator/2 active idle 10.1.0.201 metrics-generator ready. -tempo-querier/0* active idle 10.1.0.118 querier ready. -tempo-querier/1 active idle 10.1.0.195 querier ready. -tempo-querier/2 active idle 10.1.0.197 querier ready. -tempo-query-frontend/0* active idle 10.1.0.17 query-frontend ready. -tempo-query-frontend/1 active idle 10.1.0.180 query-frontend ready. -tempo-query-frontend/2 active idle 10.1.0.12 query-frontend ready. -tempo-s3-integrator/0* active idle 10.1.0.247 -tempo/0* active idle 10.1.0.217 -tempo/1 active idle 10.1.0.84 -tempo/2 active idle 10.1.0.249 -traefik/0* active idle 10.1.0.127 Serving at 10.63.93.138 - -Offer Application Charm Rev Connected Endpoint Interface Role -alertmanager-karma-dashboard alertmanager alertmanager-k8s 171 0/0 karma-dashboard karma_dashboard provider -grafana-dashboards grafana grafana-k8s 155 0/0 grafana-dashboard grafana_dashboard requirer -loki-logging loki loki-coordinator-k8s 35 0/0 logging loki_push_api provider -mimir-receive-remote-write mimir mimir-coordinator-k8s 57 0/0 receive-remote-write prometheus_remote_write provider +cos ck8s ck8s 3.6.21 unsupported 12:14:38+02:00 + +App Version Status Scale Charm Channel Rev Address Exposed Message +alertmanager 0.31.0 active 1 alertmanager-k8s dev/edge 206 10.152.183.104 no +ca active 1 self-signed-certificates 1/edge 637 10.152.183.157 no +catalogue active 1 catalogue-k8s dev/edge 125 10.152.183.162 no +grafana 12.4.2 active 1 grafana-k8s dev/edge 187 10.152.183.83 no +loki active 1 loki-coordinator-k8s dev/edge 61 10.152.183.88 no Degraded. +loki-backend 3.7.1 active 1 loki-worker-k8s dev/edge 68 10.152.183.90 no backend ready. +loki-read 3.7.1 active 1 loki-worker-k8s dev/edge 68 10.152.183.211 no read ready. +loki-s3-integrator active 1 s3-integrator 2/edge 550 10.152.183.129 no +loki-write 3.7.1 active 1 loki-worker-k8s dev/edge 68 10.152.183.217 no write ready. +mimir active 1 mimir-coordinator-k8s dev/edge 81 10.152.183.220 no +mimir-backend 2.17.10 active 1 mimir-worker-k8s dev/edge 71 10.152.183.212 no backend ready. +mimir-read 2.17.10 active 1 mimir-worker-k8s dev/edge 71 10.152.183.227 no read ready. +mimir-s3-integrator active 1 s3-integrator 2/edge 550 10.152.183.100 no +mimir-write 2.17.10 active 1 mimir-worker-k8s dev/edge 71 10.152.183.193 no write ready. +otelcol 0.130.1 active 1 opentelemetry-collector-k8s dev/edge 179 10.152.183.115 no +tempo active 1 tempo-coordinator-k8s dev/edge 149 10.152.183.117 no +tempo-compactor 2.10.1 active 1 tempo-worker-k8s dev/edge 102 10.152.183.200 no compactor ready. +tempo-distributor 2.10.1 active 1 tempo-worker-k8s dev/edge 102 10.152.183.228 no distributor ready. +tempo-ingester 2.10.1 active 1 tempo-worker-k8s dev/edge 102 10.152.183.92 no ingester ready. +tempo-metrics-generator 2.10.1 active 1 tempo-worker-k8s dev/edge 102 10.152.183.183 no metrics-generator ready. +tempo-querier 2.10.1 active 1 tempo-worker-k8s dev/edge 102 10.152.183.137 no querier ready. +tempo-query-frontend 2.10.1 active 1 tempo-worker-k8s dev/edge 102 10.152.183.20 no query-frontend ready. +tempo-s3-integrator active 1 s3-integrator 2/edge 550 10.152.183.216 no +traefik 2.11.0 active 1 traefik-k8s latest/edge 294 10.152.183.182 no Serving at http://10.249.85.241 + +Unit Workload Agent Address Ports Message +alertmanager/0* active idle 10.1.0.112 +ca/0* active idle 10.1.0.180 +catalogue/0* active idle 10.1.0.186 +grafana/0* active idle 10.1.0.169 +loki-backend/0* active idle 10.1.0.142 backend ready. +loki-read/0* active idle 10.1.0.225 read ready. +loki-s3-integrator/0* active idle 10.1.0.96 +loki-write/0* active idle 10.1.0.229 write ready. +loki/0* active idle 10.1.0.69 Degraded. +mimir-backend/0* active idle 10.1.0.218 backend ready. +mimir-read/0* active idle 10.1.0.88 read ready. +mimir-s3-integrator/0* active idle 10.1.0.64 +mimir-write/0* active idle 10.1.0.43 write ready. +mimir/0* active idle 10.1.0.57 +otelcol/0* active idle 10.1.0.121 +tempo-compactor/0* active idle 10.1.0.109 compactor ready. +tempo-distributor/0* active idle 10.1.0.30 distributor ready. +tempo-ingester/0* active idle 10.1.0.155 ingester ready. +tempo-metrics-generator/0* active idle 10.1.0.124 metrics-generator ready. +tempo-querier/0* active idle 10.1.0.106 querier ready. +tempo-query-frontend/0* active idle 10.1.0.114 query-frontend ready. +tempo-s3-integrator/0* active idle 10.1.0.81 +tempo/0* active idle 10.1.0.107 +traefik/0* active idle 10.1.0.135 Serving at http://10.249.85.241 + +Offer Application Charm Rev Connected Endpoint Interface Role +alertmanager-karma-dashboard alertmanager alertmanager-k8s 206 0/0 karma-dashboard karma_dashboard provider +certificates ca self-signed-certificates 637 0/0 certificates tls-certificates provider +grafana-dashboards grafana grafana-k8s 187 0/0 grafana-dashboard grafana_dashboard requirer +loki-logging loki loki-coordinator-k8s 61 0/0 logging loki_push_api provider +mimir-receive-remote-write mimir mimir-coordinator-k8s 81 0/0 receive-remote-write prometheus_remote_write provider +send-ca-cert ca self-signed-certificates 637 0/0 send-ca-cert certificate_transfer provider + +Integration provider Requirer Interface Type Message +alertmanager:alerting loki:alertmanager alertmanager_dispatch regular +alertmanager:alerting mimir:alertmanager alertmanager_dispatch regular +alertmanager:grafana-dashboard grafana:grafana-dashboard grafana_dashboard regular +alertmanager:grafana-source grafana:grafana-source grafana_datasource regular +alertmanager:replicas alertmanager:replicas alertmanager_replica peer +alertmanager:self-metrics-endpoint otelcol:metrics-endpoint prometheus_scrape regular +ca:certificates alertmanager:certificates tls-certificates regular +ca:certificates catalogue:certificates tls-certificates regular +ca:certificates grafana:certificates tls-certificates regular +ca:certificates loki:certificates tls-certificates regular +ca:certificates mimir:certificates tls-certificates regular +ca:certificates otelcol:receive-server-cert tls-certificates regular +ca:certificates tempo:certificates tls-certificates regular +ca:send-ca-cert traefik:receive-ca-cert certificate_transfer regular +catalogue:catalogue alertmanager:catalogue catalogue regular +catalogue:catalogue grafana:catalogue catalogue regular +catalogue:catalogue mimir:catalogue catalogue regular +catalogue:catalogue tempo:catalogue catalogue regular +catalogue:replicas catalogue:replicas catalogue_replica peer +grafana:grafana grafana:grafana grafana_peers peer +grafana:replicas grafana:replicas grafana_replicas peer +loki-s3-integrator:s3-credentials loki:s3 s3 regular +loki-s3-integrator:status-peers loki-s3-integrator:status-peers status_peers peer +loki:grafana-dashboards-provider grafana:grafana-dashboard grafana_dashboard regular +loki:grafana-source grafana:grafana-source grafana_datasource regular +loki:logging otelcol:send-loki-logs loki_push_api regular +loki:loki-cluster loki-backend:loki-cluster loki_cluster regular +loki:loki-cluster loki-read:loki-cluster loki_cluster regular +loki:loki-cluster loki-write:loki-cluster loki_cluster regular +loki:loki-peers loki:loki-peers loki_peers peer +loki:self-metrics-endpoint otelcol:metrics-endpoint prometheus_scrape regular +loki:send-datasource tempo:receive-datasource grafana_datasource_exchange regular +mimir-s3-integrator:s3-credentials mimir:s3 s3 regular +mimir-s3-integrator:status-peers mimir-s3-integrator:status-peers status_peers peer +mimir:grafana-dashboards-provider grafana:grafana-dashboard grafana_dashboard regular +mimir:grafana-source grafana:grafana-source grafana_datasource regular +mimir:mimir-cluster mimir-backend:mimir-cluster mimir_cluster regular +mimir:mimir-cluster mimir-read:mimir-cluster mimir_cluster regular +mimir:mimir-cluster mimir-write:mimir-cluster mimir_cluster regular +mimir:mimir-peers mimir:mimir-peers mimir_peers peer +mimir:receive-remote-write otelcol:send-remote-write prometheus_remote_write regular +mimir:receive-remote-write tempo:send-remote-write prometheus_remote_write regular +mimir:self-metrics-endpoint otelcol:metrics-endpoint prometheus_scrape regular +mimir:send-datasource tempo:receive-datasource grafana_datasource_exchange regular +otelcol:grafana-dashboards-provider grafana:grafana-dashboard grafana_dashboard regular +otelcol:peers otelcol:peers otelcol_replica peer +otelcol:receive-loki-logs alertmanager:logging loki_push_api regular +otelcol:receive-loki-logs grafana:logging loki_push_api regular +otelcol:receive-loki-logs loki:logging-consumer loki_push_api regular +otelcol:receive-loki-logs mimir:logging-consumer loki_push_api regular +otelcol:receive-loki-logs tempo:logging loki_push_api regular +otelcol:receive-traces grafana:charm-tracing tracing regular +otelcol:receive-traces loki:charm-tracing tracing regular +otelcol:receive-traces mimir:charm-tracing tracing regular +tempo-s3-integrator:s3-credentials tempo:s3 s3 regular +tempo-s3-integrator:status-peers tempo-s3-integrator:status-peers status_peers peer +tempo:grafana-dashboard grafana:grafana-dashboard grafana_dashboard regular +tempo:grafana-source grafana:grafana-source grafana_datasource regular +tempo:metrics-endpoint otelcol:metrics-endpoint prometheus_scrape regular +tempo:peers tempo:peers tempo_peers peer +tempo:tempo-cluster tempo-compactor:tempo-cluster tempo_cluster regular +tempo:tempo-cluster tempo-distributor:tempo-cluster tempo_cluster regular +tempo:tempo-cluster tempo-ingester:tempo-cluster tempo_cluster regular +tempo:tempo-cluster tempo-metrics-generator:tempo-cluster tempo_cluster regular +tempo:tempo-cluster tempo-querier:tempo-cluster tempo_cluster regular +tempo:tempo-cluster tempo-query-frontend:tempo-cluster tempo_cluster regular +tempo:tracing otelcol:send-traces tracing regular +traefik:ingress alertmanager:ingress ingress regular +traefik:ingress catalogue:ingress ingress regular +traefik:ingress grafana:ingress ingress regular +traefik:ingress loki:ingress ingress regular +traefik:ingress mimir:ingress ingress regular +traefik:peers traefik:peers traefik_peers peer +traefik:traefik-route otelcol:ingress traefik_route regular +traefik:traefik-route tempo:ingress traefik_route regular + +Storage Unit Storage ID Type Pool Mountpoint Size Status Message +alertmanager/0 data/1 filesystem kubernetes /var/lib/juju/storage/data/0 1.0 GiB attached Successfully provisioned volume pvc-306b0833-5c2a-4270-ba20-fac620b8b5be +grafana/0 database/3 filesystem kubernetes /var/lib/juju/storage/database/0 1.0 GiB attached Successfully provisioned volume pvc-b37d7f49-0b5a-409f-8e5b-56a57d212598 +loki-backend/0 loki-persisted/12 filesystem kubernetes /var/lib/juju/storage/loki-persisted/0 1.0 GiB attached Successfully provisioned volume pvc-9b92a901-ac30-4e26-a9c0-81692aecef8c +loki-read/0 loki-persisted/6 filesystem kubernetes /var/lib/juju/storage/loki-persisted/0 1.0 GiB attached Successfully provisioned volume pvc-d24b4519-f3f6-4cc7-b595-7d4c3bb19d75 +loki-write/0 loki-persisted/13 filesystem kubernetes /var/lib/juju/storage/loki-persisted/0 1.0 GiB attached Successfully provisioned volume pvc-4d8a0605-9cad-415a-b55d-f96bc561bb4f +mimir-backend/0 data/7 filesystem kubernetes /var/lib/juju/storage/data/0 1.0 GiB attached Successfully provisioned volume pvc-c2335bbf-0b4b-4509-be45-e6915af0cb87 +mimir-backend/0 recovery-data/8 filesystem kubernetes /var/lib/juju/storage/recovery-data/0 1.0 GiB attached Successfully provisioned volume pvc-ca30d59d-210c-466a-9b0e-956f0b0fc875 +mimir-read/0 data/10 filesystem kubernetes /var/lib/juju/storage/data/0 1.0 GiB attached Successfully provisioned volume pvc-bf7e55a0-f530-47a6-8db8-82f67353bf60 +mimir-read/0 recovery-data/11 filesystem kubernetes /var/lib/juju/storage/recovery-data/0 1.0 GiB attached Successfully provisioned volume pvc-f05111ec-90d4-4d34-b46c-11665f25cbd6 +mimir-write/0 data/4 filesystem kubernetes /var/lib/juju/storage/data/0 1.0 GiB attached Successfully provisioned volume pvc-f15365ba-cc3c-46ac-b770-5baa060091c7 +mimir-write/0 recovery-data/5 filesystem kubernetes /var/lib/juju/storage/recovery-data/0 1.0 GiB attached Successfully provisioned volume pvc-4d0af7f9-ae4c-4396-b047-d5d26bf1de7c +otelcol/0 persisted/2 filesystem kubernetes /var/lib/juju/storage/persisted/0 1.0 GiB attached Successfully provisioned volume pvc-64e14ebe-2e64-484a-9fc1-486d42e5c57c +tempo-compactor/0 wal/18 filesystem kubernetes /var/lib/juju/storage/wal/0 1.0 GiB attached Successfully provisioned volume pvc-d51e4cf4-5ffe-4ffa-b291-d6973b7a1f3f +tempo-distributor/0 wal/9 filesystem kubernetes /var/lib/juju/storage/wal/0 1.0 GiB attached Successfully provisioned volume pvc-493ddbc3-6db7-4212-8eac-24f113c34417 +tempo-ingester/0 wal/16 filesystem kubernetes /var/lib/juju/storage/wal/0 1.0 GiB attached Successfully provisioned volume pvc-02b1e04d-af1e-4103-af93-350186f93a1c +tempo-metrics-generator/0 wal/14 filesystem kubernetes /var/lib/juju/storage/wal/0 1.0 GiB attached Successfully provisioned volume pvc-008f0c76-fbd1-4862-b116-f2a116c6f68f +tempo-querier/0 wal/15 filesystem kubernetes /var/lib/juju/storage/wal/0 1.0 GiB attached Successfully provisioned volume pvc-5b12acbb-af86-471c-8875-2196276cbcd5 +tempo-query-frontend/0 wal/17 filesystem kubernetes /var/lib/juju/storage/wal/0 1.0 GiB attached Successfully provisioned volume pvc-e5cde942-0ada-4dc8-9ade-fe6989d1f7a0 +traefik/0 configurations/0 filesystem kubernetes /var/lib/juju/storage/configurations/0 1.0 GiB attached Successfully provisioned volume pvc-5930d32c-7909-4d81-b039-ab45c3461200 ``` diff --git a/docs/tutorial/cos-lite-canonical-k8s-sandbox.conf b/docs/tutorial/cos-lite-canonical-k8s-sandbox.conf deleted file mode 100644 index 2856bc86..00000000 --- a/docs/tutorial/cos-lite-canonical-k8s-sandbox.conf +++ /dev/null @@ -1,34 +0,0 @@ -#cloud-config - -# Usage example: -# multipass launch noble --cloud-init ./cos-lite-canonical-k8s-sandbox.conf --name cos --memory 8G --cpus 4 --disk 40G -# multipass exec cos -- tail -f /var/log/cloud-init-output.log - -snap: - commands: - - snap install juju --channel=3.6/stable - - snap install k8s --classic --channel=1.33-classic/stable - -runcmd: - - | - echo "Setting up K8s..." - k8s bootstrap - k8s status --wait-ready - k8s enable local-storage - k8s enable load-balancer - k8s set load-balancer.l2-mode=true load-balancer.cidrs="$(ip -4 -j route get 2.2.2.2 | jq -r '.[] | .prefsrc')/32" - k8s status --wait-ready - - echo "Adding K8s cloud to Juju..." - k8s kubectl config view --raw | sudo -u ubuntu /snap/juju/current/bin/juju add-k8s ck8s --client - echo "Bootstrapping K8s controller..." - sudo -u ubuntu juju bootstrap ck8s - - echo "Deploying COS Lite..." - sudo -u ubuntu juju add-model cos - sudo -u ubuntu curl -o ~ubuntu/offers-overlay.yaml -fL https://raw.githubusercontent.com/canonical/cos-lite-bundle/main/overlays/offers-overlay.yaml - sudo -u ubuntu juju deploy cos-lite --trust --overlay ~ubuntu/offers-overlay.yaml - echo "Waiting for model to settle..." - sudo -u ubuntu juju wait-for model cos --query='forEach(units, unit => unit.agent-status == "idle") && forEach(applications, app => app.status == "active")' --timeout=10m - -final_message: "The COS Lite appliance is ready, after $UPTIME seconds" diff --git a/docs/tutorial/cos-lite-canonical-k8s-sandbox.md b/docs/tutorial/cos-lite-canonical-k8s-sandbox.md index 39dbe7d6..3a7287a6 100644 --- a/docs/tutorial/cos-lite-canonical-k8s-sandbox.md +++ b/docs/tutorial/cos-lite-canonical-k8s-sandbox.md @@ -8,8 +8,6 @@ myst: In this tutorial you deploy a single-node COS Lite appliance, backed by hostPath storage. -You can reproduce the entire tutorial with a [cloud-config](cos-lite-canonical-k8s-sandbox.conf) script. - ## Prerequisites - A 4cpu8gb node or better, with at least 40Gi disk space (see [Sizing guide](../reference/system-requirements) for production deployments). @@ -21,22 +19,27 @@ You can reproduce the entire tutorial with a [cloud-config](cos-lite-canonical-k - K8s cloud added to Juju ([doc](https://documentation.ubuntu.com/juju/3.6/howto/manage-clouds/#add-a-kubernetes-cloud)). -## Deploy the COS Lite bundle +## Deploy COS Lite using Terraform + +To deploy the COS Lite solution, create this root module and set the `model` to `cos-lite`: -It is usually a good idea to create a dedicated model for the COS Lite bundle. So let's do just that and call the new model `cos`: +```{literalinclude} ../../tests/integration/cos_lite/tls_internal/track-dev.tf +``` -Create a new juju model, `cos`: +```{dropdown} Customize COS Lite +You can customize the deployment of COS Lite by overriding the defaults of these variables: -```bash -$ juju add-model cos +```{literalinclude} ../../terraform/cos-lite/variables.tf ``` -Next, deploy the bundle with: +Then, use terraform to deploy the module: -```bash -$ juju deploy cos-lite --trust +```shell +terraform init +terraform apply -var="model=cos-lite" ``` + You can watch the model as it settles with: ```bash @@ -47,88 +50,87 @@ The status of your deployment should eventually be very similar to the following ``` $ juju status --relations --storage -Model Controller Cloud/Region Version SLA Timestamp -cos ck8s ck8s 3.6.6 unsupported 16:44:44-04:00 - -App Version Status Scale Charm Channel Rev Address Exposed Message -alertmanager 0.27.0 active 1 alertmanager-k8s 1/stable 160 10.152.183.253 no -catalogue active 1 catalogue-k8s 1/stable 81 10.152.183.181 no -grafana 9.5.3 active 1 grafana-k8s 1/stable 143 10.152.183.152 no -loki 2.9.6 active 1 loki-k8s 1/stable 190 10.152.183.176 no -prometheus 2.52.0 active 1 prometheus-k8s latest/stable 234 10.152.183.54 no -traefik 2.11.0 active 1 traefik-k8s latest/stable 236 10.152.183.56 no Serving at 10.63.93.172 - -Unit Workload Agent Address Ports Message -alertmanager/0* active idle 10.1.0.221 -catalogue/0* active idle 10.1.0.225 -grafana/0* active idle 10.1.0.129 -loki/0* active idle 10.1.0.72 -prometheus/0* active idle 10.1.0.60 -traefik/0* active idle 10.1.0.65 Serving at 10.63.93.172 + +Model Controller Cloud/Region Version SLA Timestamp +cos-lite kub k8s 3.6.21 unsupported 11:39:52+02:00 + +App Version Status Scale Charm Channel Rev Address Exposed Message +alertmanager 0.31.0 active 1 alertmanager-k8s dev/edge 202 10.152.183.170 no +ca active 1 self-signed-certificates 1/edge 637 10.152.183.71 no +catalogue active 1 catalogue-k8s dev/edge 125 10.152.183.24 no +grafana 12.4.2 active 1 grafana-k8s dev/edge 186 10.152.183.59 no +loki 3.7.1 active 1 loki-k8s dev/edge 226 10.152.183.29 no +prometheus 3.11.1 active 1 prometheus-k8s dev/edge 292 10.152.183.104 no +traefik 2.11.0 active 1 traefik-k8s latest/edge 292 10.152.183.108 no Serving at http://192.168.178.192 + +Unit Workload Agent Address Ports Message +alertmanager/0* active idle 10.1.0.207 +ca/0* active idle 10.1.0.155 +catalogue/0* active idle 10.1.0.193 +grafana/0* active idle 10.1.0.229 +loki/0* active idle 10.1.0.133 +prometheus/0* active idle 10.1.0.83 +traefik/0* active idle 10.1.0.93 Serving at http://192.168.178.192 + +Offer Application Charm Rev Connected Endpoint Interface Role +alertmanager-karma-dashboard alertmanager alertmanager-k8s 202 0/0 karma-dashboard karma_dashboard provider +certificates ca self-signed-certificates 637 0/0 certificates tls-certificates provider +grafana-dashboards grafana grafana-k8s 186 0/0 grafana-dashboard grafana_dashboard requirer +loki-logging loki loki-k8s 226 0/0 logging loki_push_api provider +prometheus-metrics-endpoint prometheus prometheus-k8s 292 0/0 metrics-endpoint prometheus_scrape requirer +prometheus-receive-remote-write prometheus prometheus-k8s 292 0/0 receive-remote-write prometheus_remote_write provider +send-ca-cert ca self-signed-certificates 637 0/0 send-ca-cert certificate_transfer provider Integration provider Requirer Interface Type Message -alertmanager:alerting loki:alertmanager alertmanager_dispatch regular -alertmanager:alerting prometheus:alertmanager alertmanager_dispatch regular -alertmanager:grafana-dashboard grafana:grafana-dashboard grafana_dashboard regular -alertmanager:grafana-source grafana:grafana-source grafana_datasource regular -alertmanager:replicas alertmanager:replicas alertmanager_replica peer -alertmanager:self-metrics-endpoint prometheus:metrics-endpoint prometheus_scrape regular -catalogue:catalogue alertmanager:catalogue catalogue regular -catalogue:catalogue grafana:catalogue catalogue regular -catalogue:catalogue prometheus:catalogue catalogue regular -catalogue:replicas catalogue:replicas catalogue_replica peer -grafana:grafana grafana:grafana grafana_peers peer -grafana:metrics-endpoint prometheus:metrics-endpoint prometheus_scrape regular -grafana:replicas grafana:replicas grafana_replicas peer -loki:grafana-dashboard grafana:grafana-dashboard grafana_dashboard regular -loki:grafana-source grafana:grafana-source grafana_datasource regular -loki:metrics-endpoint prometheus:metrics-endpoint prometheus_scrape regular -loki:replicas loki:replicas loki_replica peer -prometheus:grafana-dashboard grafana:grafana-dashboard grafana_dashboard regular -prometheus:grafana-source grafana:grafana-source grafana_datasource regular -prometheus:prometheus-peers prometheus:prometheus-peers prometheus_peers peer -traefik:ingress alertmanager:ingress ingress regular -traefik:ingress catalogue:ingress ingress regular -traefik:ingress-per-unit loki:ingress ingress_per_unit regular -traefik:ingress-per-unit prometheus:ingress ingress_per_unit regular -traefik:metrics-endpoint prometheus:metrics-endpoint prometheus_scrape regular -traefik:peers traefik:peers traefik_peers peer -traefik:traefik-route grafana:ingress traefik_route regular +alertmanager:alerting loki:alertmanager alertmanager_dispatch regular +alertmanager:alerting prometheus:alertmanager alertmanager_dispatch regular +alertmanager:grafana-dashboard grafana:grafana-dashboard grafana_dashboard regular +alertmanager:grafana-source grafana:grafana-source grafana_datasource regular +alertmanager:replicas alertmanager:replicas alertmanager_replica peer +alertmanager:self-metrics-endpoint prometheus:metrics-endpoint prometheus_scrape regular +ca:certificates alertmanager:certificates tls-certificates regular +ca:certificates catalogue:certificates tls-certificates regular +ca:certificates grafana:certificates tls-certificates regular +ca:certificates loki:certificates tls-certificates regular +ca:certificates prometheus:certificates tls-certificates regular +ca:send-ca-cert traefik:receive-ca-cert certificate_transfer regular +catalogue:catalogue alertmanager:catalogue catalogue regular +catalogue:catalogue grafana:catalogue catalogue regular +catalogue:catalogue prometheus:catalogue catalogue regular +catalogue:replicas catalogue:replicas catalogue_replica peer +grafana:grafana grafana:grafana grafana_peers peer +grafana:metrics-endpoint prometheus:metrics-endpoint prometheus_scrape regular +grafana:replicas grafana:replicas grafana_replicas peer +loki:grafana-dashboard grafana:grafana-dashboard grafana_dashboard regular +loki:grafana-source grafana:grafana-source grafana_datasource regular +loki:metrics-endpoint prometheus:metrics-endpoint prometheus_scrape regular +loki:replicas loki:replicas loki_replica peer +prometheus:grafana-dashboard grafana:grafana-dashboard grafana_dashboard regular +prometheus:grafana-source grafana:grafana-source grafana_datasource regular +prometheus:prometheus-peers prometheus:prometheus-peers prometheus_peers peer +traefik:ingress alertmanager:ingress ingress regular +traefik:ingress catalogue:ingress ingress regular +traefik:ingress grafana:ingress ingress regular +traefik:ingress-per-unit loki:ingress ingress_per_unit regular +traefik:ingress-per-unit prometheus:ingress ingress_per_unit regular +traefik:metrics-endpoint prometheus:metrics-endpoint prometheus_scrape regular +traefik:peers traefik:peers traefik_peers peer Storage Unit Storage ID Type Pool Mountpoint Size Status Message -alertmanager/0 data/0 filesystem kubernetes /var/lib/juju/storage/data/0 1.0 GiB attached Successfully provisioned volume pvc-eb1dc923-32a0-4729-9ec8-694b50672987 -grafana/0 database/1 filesystem kubernetes /var/lib/juju/storage/database/0 1.0 GiB attached Successfully provisioned volume pvc-b5df8210-671a-4ed6-a083-3fe50e3c6fdc -loki/0 active-index-directory/2 filesystem kubernetes /var/lib/juju/storage/active-index-directory/0 1.0 GiB attached Successfully provisioned volume pvc-af183528-4399-42c2-ae59-94e71e1a18c9 -loki/0 loki-chunks/3 filesystem kubernetes /var/lib/juju/storage/loki-chunks/0 1.0 GiB attached Successfully provisioned volume pvc-86caedeb-e0a5-438f-9ab9-d118f5629723 -prometheus/0 database/4 filesystem kubernetes /var/lib/juju/storage/database/0 1.0 GiB attached Successfully provisioned volume pvc-eaece84a-5b45-4f08-b82f-5eb07163d637 -traefik/0 configurations/5 filesystem kubernetes /var/lib/juju/storage/configurations/0 1.0 GiB attached Successfully provisioned volume pvc-4b1da33c-e66f-42bf-a12f-4ac11806a63a +alertmanager/0 data/8 filesystem kubernetes /var/lib/juju/storage/data/0 1.0 GiB attached Successfully provisioned volume pvc-cf00986d-5428-467b-89f9-a0788e996702 +grafana/0 database/6 filesystem kubernetes /var/lib/juju/storage/database/0 1.0 GiB attached Successfully provisioned volume pvc-eddf2615-6147-4e78-8224-fcf69e8080c5 +loki/0 active-index-directory/4 filesystem kubernetes /var/lib/juju/storage/active-index-directory/0 1.0 GiB attached Successfully provisioned volume pvc-3d7b9b8d-1d50-469c-b5ad-94201e517d41 +loki/0 loki-chunks/5 filesystem kubernetes /var/lib/juju/storage/loki-chunks/0 1.0 GiB attached Successfully provisioned volume pvc-aa88c858-aa5f-4981-8330-7eb8245ea581 +prometheus/0 database/3 filesystem kubernetes /var/lib/juju/storage/database/0 1.0 GiB attached Successfully provisioned volume pvc-e8ae59b8-e046-4041-9a44-9bde5b9950ae +traefik/0 configurations/2 filesystem kubernetes /var/lib/juju/storage/configurations/0 1.0 GiB attached Successfully provisioned volume pvc-29e9fd9a-b787-4056-9cd3-5135b6a1c442 ``` Now COS Lite is good to go: you can relate software with it to begin the monitoring! -## Add "offers" to enable cross-model relations - -Download the [offers](https://github.com/canonical/cos-lite-bundle/blob/main/overlays/offers-overlay.yaml) -[overlay](https://documentation.ubuntu.com/charmcraft/stable/reference/files/bundle-yaml-file/), +Obtain the Grafana admin password, ```bash -curl -L https://raw.githubusercontent.com/canonical/cos-lite-bundle/main/overlays/offers-overlay.yaml -O -``` - - and update the deployment: - -```bash -juju deploy cos-lite --trust --overlay ./offers-overlay.yaml -``` - -This enables [cross-model relations](https://documentation.ubuntu.com/juju/3.6/reference/relation/#cross-model-relation). -In the output of `juju status` you should now see the following new section: - -``` -Offer Application Charm Rev Connected Endpoint Interface Role -alertmanager-karma-dashboard alertmanager alertmanager-k8s 160 0/0 karma-dashboard karma_dashboard provider -grafana-dashboards grafana grafana-k8s 143 0/0 grafana-dashboard grafana_dashboard requirer -loki-logging loki loki-k8s 190 0/0 logging loki_push_api provider -prometheus-receive-remote-write prometheus prometheus-k8s 234 0/0 receive-remote-write prometheus_remote_write provider +juju run grafana/leader get-admin-password ``` +then head over to the URL listed in the output and use the provided password to log in. diff --git a/docs/tutorial/cos-lite-microk8s-sandbox.md b/docs/tutorial/cos-lite-microk8s-sandbox.md index f3003c08..929c72e8 100644 --- a/docs/tutorial/cos-lite-microk8s-sandbox.md +++ b/docs/tutorial/cos-lite-microk8s-sandbox.md @@ -182,18 +182,22 @@ $ juju deploy cos-lite \ ## Deploy COS Lite using Terraform -Create a `cos-lite-microk8s-sandbox.tf` file with the following Terraform module, or include it in your Terraform plan: +To deploy the COS Lite solution, create this root module and set the `model` to `cos-lite`: -```{literalinclude} /tutorial/cos-lite-microk8s-sandbox.tf +```{literalinclude} ../../tests/integration/cos_lite/tls_internal/track-dev.tf ``` -**Note**: You can customize further the revisions of each charm and other aspects of COS Lite: have a look at the [`variables.tf`](../../terraform/cos-lite/variables.tf) file of the COS Lite Terraform module for the complete documentation. +```{dropdown} Customize COS Lite +You can customize the deployment of COS Lite by overriding the defaults of these variables: + +```{literalinclude} ../../terraform/cos-lite/variables.tf +``` -It is usually a good idea to create a dedicated model for COS Lite. This module creates one named `cos`, which you can override. +It is usually a good idea to create a dedicated model for COS Lite. This module creates one named `cos-lite`. If there are multiple clouds registered in the controller or a default cloud is not setup, an explicit reference of the cloud in the base terraform file needs to exist. Look at [how to reference a K8s cloud](../how-to/configure-and-tune/reference-k8s-cloud-for-cos.md) section. @@ -207,7 +211,7 @@ Next, deploy COS Lite in the new model, run: ```bash $ terraform init -$ terraform apply # verify the changes you're applying before accepting! +$ terraform apply -var="model=cos-lite" ``` Now you can sit back and watch the deployment take place: diff --git a/docs/tutorial/cos-lite-microk8s-sandbox.tf b/docs/tutorial/cos-lite-microk8s-sandbox.tf deleted file mode 100644 index 853c42df..00000000 --- a/docs/tutorial/cos-lite-microk8s-sandbox.tf +++ /dev/null @@ -1,22 +0,0 @@ -terraform { - required_version = ">= 1.5" - required_providers { - juju = { - source = "juju/juju" - version = "~> 1.0" - } - } -} - -resource "juju_model" "cos" { - name = "cos" - config = { logging-config = "=WARNING; unit=DEBUG" } -} - -module "cos-lite" { - source = "git::https://github.com/canonical/observability-stack//terraform/cos-lite?ref=main" - model_uuid = juju_model.cos.uuid - risk = "edge" - ssc = { channel = "1/stable" } - traefik = { channel = "latest/edge" } -} diff --git a/terraform/cos-lite/README.md b/terraform/cos-lite/README.md index baef5cc0..51ac3541 100644 --- a/terraform/cos-lite/README.md +++ b/terraform/cos-lite/README.md @@ -51,20 +51,9 @@ This is a Terraform module facilitating the deployment of the COS Lite solution, ## Usage -### Using different Terraform Juju provider versions -If you require the Terraform Juju provider `< 1.0.0`, then deploy the COS Lite module with the `tf-provider-v0` tag: +The minimum version of Terraform Juju provider required is `1.5`. -```hcl -module "cos-lite" { - source = "git::https://github.com/canonical/observability-stack//terraform/cos-lite?ref=tf-provider-v0" -} -``` - -Otherwise, you can deploy from main (without `?ref`) which uses the Terraform Juju provider `~> 1.0`. See the [v1 migration documentation](https://documentation.ubuntu.com/terraform-provider-juju/v1/howto/manage-provider/upgrade-provider-to-v1/) if you need to upgrade your modules. - -### Basic usage - -To deploy the COS Lite solution in a model named `cos`, create this root module: +To deploy the COS Lite solution in a model named `cos-lite`, create this root module: ```hcl terraform { required_version = ">= 1.5" @@ -76,14 +65,14 @@ terraform { } } -resource "juju_model" "cos" { +resource "juju_model" "cos_lite" { name = "cos-lite" } module "cos-lite" { - source = "git::https://github.com/canonical/observability-stack//terraform/cos-lite?ref=track/2" - model_uuid = juju_model.cos.uuid - channel = "2/stable" + source = "git::https://github.com/canonical/observability-stack//terraform/cos-lite?ref=main" + model_uuid = juju_model.cos_lite.uuid + risk = "edge" } ``` From 043d98959bb12e7119673fedef058eb0d3fcf636 Mon Sep 17 00:00:00 2001 From: Shardul Deshpande Date: Fri, 15 May 2026 17:12:15 +0530 Subject: [PATCH 08/11] docs(cos-lite): add details to prerequisites, link strict-install docs (#348) * docs(cos-lite): add details to prerequisites, link strict-install docs * added snap_microk8s to custom_wordlist.txt --- docs/.custom_wordlist.txt | 1 + docs/tutorial/cos-lite-microk8s-sandbox.md | 35 ++++++++++++++++++---- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/docs/.custom_wordlist.txt b/docs/.custom_wordlist.txt index fc4de209..288174c3 100644 --- a/docs/.custom_wordlist.txt +++ b/docs/.custom_wordlist.txt @@ -172,6 +172,7 @@ SHA sitemapindex SLAs Snapcraft +snap_microk8s SNMP Sphinx Spread diff --git a/docs/tutorial/cos-lite-microk8s-sandbox.md b/docs/tutorial/cos-lite-microk8s-sandbox.md index 929c72e8..befa7e77 100644 --- a/docs/tutorial/cos-lite-microk8s-sandbox.md +++ b/docs/tutorial/cos-lite-microk8s-sandbox.md @@ -10,13 +10,36 @@ In this tutorial you deploy a single-node COS Lite appliance, backed by hostPath ## Prerequisites -This tutorial assumes you have a Juju controller bootstrapped on a -MicroK8s cloud that is ready to use, on a 4cpu8gb node or better, with at least 40Gi disk space -(see [Sizing guide](../reference/system-requirements) for production deployments). -Typical setup using [snaps](https://snapcraft.io/) -can be found in the [Juju docs](https://documentation.ubuntu.com/juju/3.6/howto/manage-your-deployment/). +This tutorial assumes you have: + +- A Juju controller bootstrapped on a MicroK8s cloud, running on a node with + at least 4 CPU, 8 GB RAM, and 40 GB of disk space. + + MicroK8s must be installed from a **strict** channel (not `--classic`): Juju + 3.x is itself a strictly confined snap and cannot bootstrap a classic + MicroK8s installation. See the MicroK8s + [strict install guide](https://canonical.com/microk8s/docs/install-strict) + for channel selection, and the Juju + [MicroK8s cloud reference](https://documentation.ubuntu.com/juju/3.6/reference/cloud/list-of-supported-clouds/the-microk8s-cloud-and-juju/) + for how Juju integrates with it. Your user must also be a member of the + `snap_microk8s` group. + +```{dropdown} Add your user to the snap_microk8s group + Strict MicroK8s uses the `snap_microk8s` group rather than the `microk8s` + group used by the classic snap. To add your current user and pick up the new + membership in the current shell: + + sudo usermod -a -G snap_microk8s $USER + newgrp snap_microk8s +``` + +- `jq` installed on the machine running the Juju client. It is used for JSON + parsing in the `metallb` step below and is not preinstalled on a fresh + Ubuntu image. Install it with: -Follow the instructions there to install Juju and MicroK8s. +```bash +sudo apt-get update && sudo apt-get install -y jq +``` ## Introduction From 3cc46916d12ef69d33ce102e49f1acbff6139247 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Fri, 15 May 2026 18:21:23 +0200 Subject: [PATCH 09/11] refactor(docs): Remove prompts from docs (#342) --- .github/prompts/review-docs.prompt.md | 1 + .../telemetry/logging-architecture.md | 4 +- .../explanation/telemetry/telemetry-labels.md | 6 +-- .../disable-charmed-rules.md | 2 +- ...electively-drop-telemetry-scrape-config.md | 2 +- .../integrate/add-tracing-to-cos-lite.md | 44 +++++++++--------- .../how-to/integrate/configure-scrape-jobs.md | 15 +++--- .../deploy-s3-integrator-and-minio.md | 18 ++++---- .../integrate/exposing-a-metrics-endpoint.md | 2 +- ...ng-cos-lite-with-uncharmed-applications.md | 6 +-- .../troubleshooting.md | 40 ++++++++-------- .../cos-lite-canonical-k8s-sandbox.md | 8 ++-- docs/tutorial/cos-lite-microk8s-sandbox.md | 46 +++++++++---------- 13 files changed, 98 insertions(+), 96 deletions(-) diff --git a/.github/prompts/review-docs.prompt.md b/.github/prompts/review-docs.prompt.md index 7f9f49f5..daa908c2 100644 --- a/.github/prompts/review-docs.prompt.md +++ b/.github/prompts/review-docs.prompt.md @@ -16,6 +16,7 @@ Review the documentation for clarity, completeness, and accuracy. - Spelling according to US English conventions. - Changes in docs should be reflected in the glossary, if the changes are related to terms/concepts that are unique to COS or charmed observability (don't redefine all/general terms). +- DO NOT use prompt marks (e.g. $ or #) in code samples. ## Context diff --git a/docs/explanation/telemetry/logging-architecture.md b/docs/explanation/telemetry/logging-architecture.md index f6f990f8..79bda5be 100644 --- a/docs/explanation/telemetry/logging-architecture.md +++ b/docs/explanation/telemetry/logging-architecture.md @@ -89,7 +89,7 @@ relations: this results in an auto-render Promtail config file with three scrape jobs, one for each "filename": ```bash -$ juju ssh --container postgresql pgsql/0 cat /etc/promtail/promtail_config.yaml +juju ssh --container postgresql pgsql/0 cat /etc/promtail/promtail_config.yaml ``` ```yaml @@ -163,7 +163,7 @@ relations: This results in an auto-generated `/etc/otelcol/config.d/otelcol_0.yaml` config file with juju topology labels and the default scrape jobs for `/var/log/**/*log` and `journalctl`: ```bash -$ juju ssh otelcol/0 cat /etc/otelcol/config.d/otelcol_0.yaml +juju ssh otelcol/0 cat /etc/otelcol/config.d/otelcol_0.yaml ``` ```yaml diff --git a/docs/explanation/telemetry/telemetry-labels.md b/docs/explanation/telemetry/telemetry-labels.md index ab7822da..da32d273 100644 --- a/docs/explanation/telemetry/telemetry-labels.md +++ b/docs/explanation/telemetry/telemetry-labels.md @@ -22,9 +22,9 @@ By convention, applications expose labeled metrics under a [`/metrics` endpoint] For example, you can run the prometheus application and curl its `:9090/metrics` endpoint to obtain the metrics exposed by the process. ```bash -$ sudo snap install prometheus +sudo snap install prometheus -$ curl localhost:9090/metrics +curl localhost:9090/metrics # -- snip -- @@ -69,7 +69,7 @@ scrape_configs: Labels that are specified under a `static_configs` entry are automatically attached to all metrics scraped from the targets: ```bash -$ curl -s --data-urlencode 'match[]={__name__="prometheus_http_requests_total"}' localhost:9090/api/v1/series | jq '.data' +curl -s --data-urlencode 'match[]={__name__="prometheus_http_requests_total"}' localhost:9090/api/v1/series | jq '.data' [ { "__name__": "prometheus_http_requests_total", diff --git a/docs/how-to/configure-and-tune/disable-charmed-rules.md b/docs/how-to/configure-and-tune/disable-charmed-rules.md index 88d3222c..9ff51bcb 100644 --- a/docs/how-to/configure-and-tune/disable-charmed-rules.md +++ b/docs/how-to/configure-and-tune/disable-charmed-rules.md @@ -24,7 +24,7 @@ using a boolean configuration option, called `forward_alert_rules`: For example, to disable forwarding of all alert rules from opentelemetry collector, ``` -$ juju config opentelemetry-collector forward_alert_rules=false +juju config opentelemetry-collector forward_alert_rules=false ``` ## Silence charmed rules using alertmanager configuration diff --git a/docs/how-to/configure-and-tune/selectively-drop-telemetry-scrape-config.md b/docs/how-to/configure-and-tune/selectively-drop-telemetry-scrape-config.md index f87d00e0..94704e8a 100644 --- a/docs/how-to/configure-and-tune/selectively-drop-telemetry-scrape-config.md +++ b/docs/how-to/configure-and-tune/selectively-drop-telemetry-scrape-config.md @@ -52,7 +52,7 @@ graph LR We can specify the `drop` action via a config option for the [scrape-config charm](https://charmhub.io/prometheus-scrape-config-k8s): ```shell -$ juju config sc metric_relabel_configs="$(cat < \ @@ -51,7 +51,7 @@ And wait for it to go to `active/idle`. ### 2. Deploy the S3 Integrator ```bash -$ juju deploy s3-integrator s3 \ +juju deploy s3-integrator s3 \ --channel edge \ --trust ``` @@ -60,7 +60,7 @@ Wait for the `s3` app to go to `blocked/idle`. The `s3` app will go into `blocked` status until you run the `sync-s3-credentials` action to give it access to `minio`. ```bash -$ juju run s3/leader sync-s3-credentials \ +juju run s3/leader sync-s3-credentials \ access-key= \ secret-key= ``` @@ -83,7 +83,7 @@ From there you should be able to create a bucket with a few clicks. See [this gu Alternatively, you can use the Minio Python SDK. ```bash -$ pip install minio +pip install minio ``` Then execute this script: @@ -111,7 +111,7 @@ if not found: Now give the `s3` app access to the bucket. ``` -$ juju config s3 \ +juju config s3 \ endpoint=minio-0.minio-endpoints..svc.cluster.local:9000 \ bucket= ``` diff --git a/docs/how-to/integrate/exposing-a-metrics-endpoint.md b/docs/how-to/integrate/exposing-a-metrics-endpoint.md index 07b8a9ad..2a424210 100644 --- a/docs/how-to/integrate/exposing-a-metrics-endpoint.md +++ b/docs/how-to/integrate/exposing-a-metrics-endpoint.md @@ -19,7 +19,7 @@ amount of code, enable your charm to get scraped by a charm like [prometheus-k8s Fetch the `prometheus_scrape` library using the `charmcraft` command: ```bash -$ charmcraft fetch-lib charms.prometheus_k8s.v0.prometheus_scrape +charmcraft fetch-lib charms.prometheus_k8s.v0.prometheus_scrape ``` ## Import the Library diff --git a/docs/how-to/integrate/integrating-cos-lite-with-uncharmed-applications.md b/docs/how-to/integrate/integrating-cos-lite-with-uncharmed-applications.md index 6ee81a43..71080e00 100644 --- a/docs/how-to/integrate/integrating-cos-lite-with-uncharmed-applications.md +++ b/docs/how-to/integrate/integrating-cos-lite-with-uncharmed-applications.md @@ -27,7 +27,7 @@ We recommend to host Opentelemetry Collector as close as possible to the workloa We recommend to install Opentelemetry Collector via a handy snap we maintain: ```bash -$ sudo snap install opentelemetry-collector +sudo snap install opentelemetry-collector ``` ```{note} @@ -53,7 +53,7 @@ In other words, Traefik's own URL needs to be stable. In the Juju model where COS Lite is deployed, run the command below to find out the URL to the proxied endpoint. ``` -$ juju run traefik/0 show-proxied-endpoints +juju run traefik/0 show-proxied-endpoints ``` Assuming you have [configured the Traefik charm](https://github.com/canonical/traefik-k8s-operator#configurations) to use an external host name, for example `"traefik.url"`, you will see something like: @@ -75,7 +75,7 @@ At this point you will need to follow [the documentation on how to configure Ope Once you've written your finished configuration to `/etc/otelcol/config.d/otelcol_0.yaml `, you'll be able to restart the snap using the following command: ```bash -$ sudo snap restart opentelemetry-collector +sudo snap restart opentelemetry-collector ``` And with that, you are done! Good job, you got this! diff --git a/docs/how-to/validate-and-troubleshoot/troubleshooting.md b/docs/how-to/validate-and-troubleshoot/troubleshooting.md index 410360eb..8163eefa 100644 --- a/docs/how-to/validate-and-troubleshoot/troubleshooting.md +++ b/docs/how-to/validate-and-troubleshoot/troubleshooting.md @@ -40,14 +40,14 @@ apply, although you will need to tailor the exact steps and commands to your set Check with: ```bash -$ microk8s status -a metallb +microk8s status -a metallb ``` If it is disabled, you can enable it with: ```bash -$ IPADDR=$(ip -4 -j route get 2.2.2.2 | jq -r '.[] | .prefsrc') -$ microk8s enable metallb:$IPADDR-$IPADDR +IPADDR=$(ip -4 -j route get 2.2.2.2 | jq -r '.[] | .prefsrc') +microk8s enable metallb:$IPADDR-$IPADDR ``` This command will fetch the IPv4 address assigned to your host, and hand it to MetalLB @@ -60,8 +60,8 @@ for instance `IPADDR=10.0.0.1-10.0.0.100`. Does the Traefik service have an external IP assigned to it? Check with: ```bash -$ JUJU_APP_NAME="traefik" -$ kubectl get svc -A -o wide | grep -E "^NAMESPACE|$JUJU_APP_NAME" +JUJU_APP_NAME="traefik" +kubectl get svc -A -o wide | grep -E "^NAMESPACE|$JUJU_APP_NAME" ``` #### No available IP in address pool @@ -77,14 +77,14 @@ This can happen when: Check with: ```bash -$ kubectl get ipaddresspool -n metallb-system -o yaml && kubectl get all -n metallb-system +kubectl get ipaddresspool -n metallb-system -o yaml && kubectl get all -n metallb-system ``` You could add more IPs to the range: ```bash -$ FROM_IP="..." TO_IP="..." -$ microk8s enable metallb:$FROM_IP-$TO_IP +FROM_IP="..." TO_IP="..." +microk8s enable metallb:$FROM_IP-$TO_IP ``` #### The Load Balancer service type reverted to `ClusterIP` @@ -95,7 +95,7 @@ Juju controller cycling may cause the type to revert from `LoadBalancer` back to Check with: ```bash -$ kubectl get svc -A -o wide | grep -E "^NAMESPACE|LoadBalancer" +kubectl get svc -A -o wide | grep -E "^NAMESPACE|LoadBalancer" ``` If Traefik isn't listed (it's not `LoadBalancer`), then recreate the pod to have it @@ -114,7 +114,7 @@ Verify that the Traefik Kubernetes service now has been assigned an external IP: ``` -$ microk8s.kubectl get services -A +microk8s.kubectl get services -A NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) cos traefik LoadBalancer 10.152.183.130 10.70.43.245 80:32343/TCP,443:30698/TCP 4d3h @@ -127,7 +127,7 @@ If you have COS Lite deployed, you may check that if works as expected using the ```bash # curl http:///-catalogue/ # for example... -$ curl http://10.70.43.245/cos-catalogue/ +curl http://10.70.43.245/cos-catalogue/ ``` This command should return a long HTML code block if everything works as expected. @@ -177,9 +177,9 @@ You can use: Make sure your alerts manifest matches the output of: ```bash -$ juju ssh prometheus/0 curl localhost:9090/api/v1/rules | jq -r '.data.groups | .[] | .rules | .[] | .name' +juju ssh prometheus/0 curl localhost:9090/api/v1/rules | jq -r '.data.groups | .[] | .rules | .[] | .name' # and... -$ juju ssh loki/0 curl localhost:3100/loki/api/v1/rules +juju ssh loki/0 curl localhost:3100/loki/api/v1/rules ``` #### Integration tests @@ -224,7 +224,7 @@ When related to Loki, make sure your logging sources are listed in: Make sure the dashboards manifest you have in the charm matches: ```bash -$ juju ssh grafana/0 curl http://admin:password@localhost:3000/api/search +juju ssh grafana/0 curl http://admin:password@localhost:3000/api/search ``` ### Data Duplication @@ -235,7 +235,7 @@ Charms should use `limit: 1` for the cos-agent relation (see example [here](http but this cannot be enforced by opentelemetry-collector itself. You can confirm this is the case with `jq`: ```bash -$ juju export-bundle | yq -o json '.' | jq -r ' +juju export-bundle | yq -o json '.' | jq -r ' .applications as $apps | .relations as $relations | $apps @@ -313,7 +313,7 @@ for agent, principals in agents.items(): Then run it using: ```bash -$ juju status --format=yaml | ./is_multi_agent.py +juju status --format=yaml | ./is_multi_agent.py ``` If there is a problem, you would see output such as: @@ -454,7 +454,7 @@ deployment itself. For MicroK8s, this would be done by increasing the limits in ### 1. Juju SSH into the machine ```bash -$ juju ssh uk8s/1 +juju ssh uk8s/1 ``` Substitute `uk8s/1` with the name of your MicroK8s unit. If you have more than @@ -465,7 +465,7 @@ one unit, you will need to repeat this for each of them. You can use whatever editor you prefer for this. In this how-to, we'll use ``vim``. ```bash -$ vim /var/snap/microk8s/current/args/containerd-env +vim /var/snap/microk8s/current/args/containerd-env ``` ### 3. Increase the `ulimit` @@ -490,7 +490,7 @@ $ vim /var/snap/microk8s/current/args/containerd-env Restart the machine the MicroK8s unit is deployed on and then wait for it to come back up. ```bash -$ sudo reboot +sudo reboot ``` ### 5. Validate @@ -499,7 +499,7 @@ Validate that the change made it through and had the desired effect once the mac back up and running. ```bash -$ juju ssh uk8s/1 cat /var/snap/microk8s/current/args/containerd-env +juju ssh uk8s/1 cat /var/snap/microk8s/current/args/containerd-env [...] diff --git a/docs/tutorial/cos-lite-canonical-k8s-sandbox.md b/docs/tutorial/cos-lite-canonical-k8s-sandbox.md index 3a7287a6..2bbc346d 100644 --- a/docs/tutorial/cos-lite-canonical-k8s-sandbox.md +++ b/docs/tutorial/cos-lite-canonical-k8s-sandbox.md @@ -42,15 +42,17 @@ terraform apply -var="model=cos-lite" You can watch the model as it settles with: -```bash -$ juju status --relations --watch=5s +```shell +juju status --relations --watch=5s ``` The status of your deployment should eventually be very similar to the following: +```shell +juju status --relations --storage ``` -$ juju status --relations --storage +``` Model Controller Cloud/Region Version SLA Timestamp cos-lite kub k8s 3.6.21 unsupported 11:39:52+02:00 diff --git a/docs/tutorial/cos-lite-microk8s-sandbox.md b/docs/tutorial/cos-lite-microk8s-sandbox.md index befa7e77..d63af89f 100644 --- a/docs/tutorial/cos-lite-microk8s-sandbox.md +++ b/docs/tutorial/cos-lite-microk8s-sandbox.md @@ -58,7 +58,7 @@ For the COS Lite bundle deployment to go smoothly, make sure the following Micro You can check this with `microk8s status`, and if any are missing, enable them with ```bash -$ microk8s enable dns +microk8s enable dns ``` ```{note} @@ -67,23 +67,23 @@ consider deploying MicroCeph on MicroK8s using this [guide](https://canonical.co ``` ```bash -$ microk8s enable hostpath-storage +microk8s enable hostpath-storage ``` The bundle comes with Traefik to provide ingress, for which you'll need a load balancer controller. If you don't have one already, the `metallb` add-on should be enabled: ```bash -$ IPADDR=$(ip -4 -j route get 2.2.2.2 | jq -r '.[] | .prefsrc') -$ microk8s enable metallb:$IPADDR-$IPADDR +IPADDR=$(ip -4 -j route get 2.2.2.2 | jq -r '.[] | .prefsrc') +microk8s enable metallb:$IPADDR-$IPADDR ``` To wait for all the add-ons to be rolled out, then run: ```bash -$ microk8s kubectl rollout status deployments/hostpath-provisioner -n kube-system -w -$ microk8s kubectl rollout status deployments/coredns -n kube-system -w -$ microk8s kubectl rollout status daemonset.apps/speaker -n metallb-system -w +microk8s kubectl rollout status deployments/hostpath-provisioner -n kube-system -w +microk8s kubectl rollout status deployments/coredns -n kube-system -w +microk8s kubectl rollout status daemonset.apps/speaker -n metallb-system -w ``` ```{note} @@ -99,26 +99,26 @@ By default, MicroK8s will use `8.8.8.8` and `8.8.4.4` as DNS servers, which can It is usually a good idea to create a dedicated model for the COS Lite bundle. So let's do just that and call the new model `cos`: ```bash -$ juju add-model cos -$ juju switch cos +juju add-model cos +juju switch cos ``` Next, deploy the bundle with: ```bash -$ juju deploy cos-lite --trust +juju deploy cos-lite --trust ``` Now you can sit back and watch the deployment take place: ```bash -$ juju status --relations --watch=5s +juju status --relations --watch=5s ``` The status of your deployment should eventually be very similar to the following: ``` -$ juju status --relations +juju status --relations Model Controller Cloud/Region Version SLA Timestamp cos microk8s microk8s/localhost 3.6.4 unsupported 15:48:47+04:00 @@ -195,9 +195,9 @@ So, if you were following the previous steps you would first need to switch to a To use any of the overlays above, you need to include an `--overlay` argument per overlay (applied in order): ```bash -$ curl -L https://raw.githubusercontent.com/canonical/cos-lite-bundle/main/overlays/offers-overlay.yaml -O -$ curl -L https://raw.githubusercontent.com/canonical/cos-lite-bundle/main/overlays/storage-small-overlay.yaml -O -$ juju deploy cos-lite \ +curl -L https://raw.githubusercontent.com/canonical/cos-lite-bundle/main/overlays/offers-overlay.yaml -O +curl -L https://raw.githubusercontent.com/canonical/cos-lite-bundle/main/overlays/storage-small-overlay.yaml -O +juju deploy cos-lite \ --trust \ --overlay ./offers-overlay.yaml \ --overlay ./storage-small-overlay.yaml @@ -233,15 +233,15 @@ To use a custom storage class for container's persistent volume during deploymen Next, deploy COS Lite in the new model, run: ```bash -$ terraform init -$ terraform apply -var="model=cos-lite" +terraform init +terraform apply -var="model=cos-lite" ``` Now you can sit back and watch the deployment take place: ```bash -$ juju switch cos -$ juju status --relations --watch=5s +juju switch cos +juju status --relations --watch=5s ``` ## Browse dashboards @@ -253,7 +253,7 @@ Traefik action. For example: ```bash -$ juju run traefik/0 show-proxied-endpoints --format=yaml \ +juju run traefik/0 show-proxied-endpoints --format=yaml \ | yq '."traefik/0".results."proxied-endpoints"' \ | jq ``` @@ -288,7 +288,7 @@ Note that Grafana does not appear in the list. Currently, to obtain Grafana's proxied endpoint you would need to look at catalogue's relation data directly - try running: ```bash -$ juju show-unit catalogue/0 | grep url +juju show-unit catalogue/0 | grep url ``` ...which should return a list of the endpoints like this: @@ -305,7 +305,7 @@ With ingress in place, you can still access the workloads via pod IPs, but you w to include the original port, as well as the ingress path. For example: ``` -$ curl 10.1.55.34:9093/cos-alertmanager/-/ready +curl 10.1.55.34:9093/cos-alertmanager/-/ready ``` The default password for Grafana is automatically generated for every installation. To @@ -313,7 +313,7 @@ access Grafana's web interface, use the username `admin`, and the password obtai from the [`get-admin-password`](https://charmhub.io/grafana-k8s/actions) action, e.g: ```bash -$ juju run grafana/leader get-admin-password --model cos +juju run grafana/leader get-admin-password --model cos ``` Enjoy! From 64eb8816998e9f849a7df1e5d88f85b2ea9a5f77 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Wed, 20 May 2026 11:47:58 -0400 Subject: [PATCH 10/11] feat: Generic COS assertions function (#351) --- .../tls_external/test_upgrade_cos_tls_external.py | 15 +++------------ .../cos/tls_full/test_upgrade_cos_tls_full.py | 15 +++------------ .../tls_internal/test_upgrade_cos_tls_internal.py | 12 +++--------- .../cos/tls_none/test_upgrade_cos_tls_none.py | 12 +++--------- .../test_upgrade_cos_lite_tls_external.py | 14 +++----------- .../tls_full/test_upgrade_cos_lite_tls_full.py | 14 +++----------- .../test_upgrade_cos_lite_tls_internal.py | 8 +++----- .../tls_none/test_upgrade_cos_lite_tls_none.py | 8 +++----- tests/integration/helpers.py | 15 +++++++++++++++ 9 files changed, 39 insertions(+), 74 deletions(-) diff --git a/tests/integration/cos/tls_external/test_upgrade_cos_tls_external.py b/tests/integration/cos/tls_external/test_upgrade_cos_tls_external.py index c360df87..33b46e17 100644 --- a/tests/integration/cos/tls_external/test_upgrade_cos_tls_external.py +++ b/tests/integration/cos/tls_external/test_upgrade_cos_tls_external.py @@ -8,12 +8,7 @@ from pathlib import Path import jubilant -from helpers import ( - catalogue_apps_are_reachable, - get_tls_context, - no_errors_in_otelcol_logs, - wait_for_active_idle_without_error, -) +from helpers import generic_assertions, no_errors_in_otelcol_logs TRACK_2_TF_FILE = Path(__file__).parent.resolve() / "track-2.tf" TRACK_DEV_TF_FILE = Path(__file__).parent.resolve() / "track-dev.tf" @@ -36,9 +31,7 @@ def test_deploy_from_track_2( # GIVEN a module deployed from track 2 tf_manager.init(TRACK_2_TF_FILE) tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model, **S3_ENDPOINT) - wait_for_active_idle_without_error([cos_model], timeout=5400) - tls_ctx = get_tls_context(tmp_path, ca_model, "self-signed-certificates") - catalogue_apps_are_reachable(cos_model, tls_ctx) + generic_assertions(cos_model, ca_model, tmp_path) no_errors_in_otelcol_logs(cos_model) @@ -50,7 +43,5 @@ def test_deploy_to_track_dev( tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model, **S3_ENDPOINT) # THEN the model is upgraded and is healthy - wait_for_active_idle_without_error([ca_model, cos_model]) - tls_ctx = get_tls_context(tmp_path, ca_model, "self-signed-certificates") - catalogue_apps_are_reachable(cos_model, tls_ctx) + generic_assertions(cos_model, ca_model, tmp_path) no_errors_in_otelcol_logs(cos_model) diff --git a/tests/integration/cos/tls_full/test_upgrade_cos_tls_full.py b/tests/integration/cos/tls_full/test_upgrade_cos_tls_full.py index a817a76b..9a48df79 100644 --- a/tests/integration/cos/tls_full/test_upgrade_cos_tls_full.py +++ b/tests/integration/cos/tls_full/test_upgrade_cos_tls_full.py @@ -8,12 +8,7 @@ from pathlib import Path import jubilant -from helpers import ( - catalogue_apps_are_reachable, - get_tls_context, - no_errors_in_otelcol_logs, - wait_for_active_idle_without_error, -) +from helpers import generic_assertions, no_errors_in_otelcol_logs TRACK_2_TF_FILE = Path(__file__).parent.resolve() / "track-2.tf" TRACK_DEV_TF_FILE = Path(__file__).parent.resolve() / "track-dev.tf" @@ -36,9 +31,7 @@ def test_deploy_from_track_2( # GIVEN a module deployed from track 2 tf_manager.init(TRACK_2_TF_FILE) tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model, **S3_ENDPOINT) - wait_for_active_idle_without_error([cos_model], timeout=5400) - tls_ctx = get_tls_context(tmp_path, ca_model, "self-signed-certificates") - catalogue_apps_are_reachable(cos_model, tls_ctx) + generic_assertions(cos_model, ca_model, tmp_path) no_errors_in_otelcol_logs(cos_model) @@ -50,7 +43,5 @@ def test_deploy_to_track_dev( tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model, **S3_ENDPOINT) # THEN the model is upgraded and is healthy - wait_for_active_idle_without_error([ca_model, cos_model]) - tls_ctx = get_tls_context(tmp_path, ca_model, "self-signed-certificates") - catalogue_apps_are_reachable(cos_model, tls_ctx) + generic_assertions(cos_model, ca_model, tmp_path) no_errors_in_otelcol_logs(cos_model) diff --git a/tests/integration/cos/tls_internal/test_upgrade_cos_tls_internal.py b/tests/integration/cos/tls_internal/test_upgrade_cos_tls_internal.py index d288c0e2..c0425362 100644 --- a/tests/integration/cos/tls_internal/test_upgrade_cos_tls_internal.py +++ b/tests/integration/cos/tls_internal/test_upgrade_cos_tls_internal.py @@ -8,11 +8,7 @@ from pathlib import Path import jubilant -from helpers import ( - catalogue_apps_are_reachable, - no_errors_in_otelcol_logs, - wait_for_active_idle_without_error, -) +from helpers import generic_assertions, no_errors_in_otelcol_logs TRACK_2_TF_FILE = Path(__file__).parent.resolve() / "track-2.tf" TRACK_DEV_TF_FILE = Path(__file__).parent.resolve() / "track-dev.tf" @@ -33,8 +29,7 @@ def test_deploy_from_track_2(tf_manager, cos_model: jubilant.Juju): # GIVEN a module deployed from track 2 tf_manager.init(TRACK_2_TF_FILE) tf_manager.apply(model=cos_model.model, **S3_ENDPOINT) - wait_for_active_idle_without_error([cos_model], timeout=5400) - catalogue_apps_are_reachable(cos_model) + generic_assertions(cos_model) no_errors_in_otelcol_logs(cos_model) @@ -44,6 +39,5 @@ def test_deploy_to_track_dev(tf_manager, cos_model: jubilant.Juju): tf_manager.apply(model=cos_model.model, **S3_ENDPOINT) # THEN the model is upgraded and is healthy - wait_for_active_idle_without_error([cos_model]) - catalogue_apps_are_reachable(cos_model) + generic_assertions(cos_model) no_errors_in_otelcol_logs(cos_model) diff --git a/tests/integration/cos/tls_none/test_upgrade_cos_tls_none.py b/tests/integration/cos/tls_none/test_upgrade_cos_tls_none.py index eaf985dd..7f33b146 100644 --- a/tests/integration/cos/tls_none/test_upgrade_cos_tls_none.py +++ b/tests/integration/cos/tls_none/test_upgrade_cos_tls_none.py @@ -8,11 +8,7 @@ from pathlib import Path import jubilant -from helpers import ( - catalogue_apps_are_reachable, - no_errors_in_otelcol_logs, - wait_for_active_idle_without_error, -) +from helpers import generic_assertions, no_errors_in_otelcol_logs TRACK_2_TF_FILE = Path(__file__).parent.resolve() / "track-2.tf" TRACK_DEV_TF_FILE = Path(__file__).parent.resolve() / "track-dev.tf" @@ -33,8 +29,7 @@ def test_deploy_from_track_2(tf_manager, cos_model: jubilant.Juju): # GIVEN a module deployed from track 2 tf_manager.init(TRACK_2_TF_FILE) tf_manager.apply(model=cos_model.model, **S3_ENDPOINT) - wait_for_active_idle_without_error([cos_model], timeout=5400) - catalogue_apps_are_reachable(cos_model) + generic_assertions(cos_model) no_errors_in_otelcol_logs(cos_model) @@ -44,6 +39,5 @@ def test_deploy_to_track_dev(tf_manager, cos_model: jubilant.Juju): tf_manager.apply(model=cos_model.model, **S3_ENDPOINT) # THEN the model is upgraded and is healthy - wait_for_active_idle_without_error([cos_model]) - catalogue_apps_are_reachable(cos_model) + generic_assertions(cos_model) no_errors_in_otelcol_logs(cos_model) 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 bded1290..46e5285a 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 @@ -7,11 +7,7 @@ from pathlib import Path import jubilant -from helpers import ( - catalogue_apps_are_reachable, - get_tls_context, - wait_for_active_idle_without_error, -) +from helpers import generic_assertions TRACK_2_TF_FILE = Path(__file__).parent.resolve() / "track-2.tf" TRACK_DEV_TF_FILE = Path(__file__).parent.resolve() / "track-dev.tf" @@ -23,9 +19,7 @@ def test_deploy_from_track_2( # GIVEN a module deployed from track 2 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], timeout=60 * 60) - tls_ctx = get_tls_context(tmp_path, ca_model, "self-signed-certificates") - catalogue_apps_are_reachable(cos_model, tls_ctx) + generic_assertions(cos_model, ca_model, tmp_path) def test_deploy_to_track_dev( @@ -36,6 +30,4 @@ def test_deploy_to_track_dev( tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model) # THEN the model is upgraded and is healthy - wait_for_active_idle_without_error([ca_model, cos_model]) - tls_ctx = get_tls_context(tmp_path, ca_model, "self-signed-certificates") - catalogue_apps_are_reachable(cos_model, tls_ctx) + generic_assertions(cos_model, ca_model, tmp_path) 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 0d742340..e834b153 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 @@ -7,11 +7,7 @@ from pathlib import Path import jubilant -from helpers import ( - catalogue_apps_are_reachable, - get_tls_context, - wait_for_active_idle_without_error, -) +from helpers import generic_assertions TRACK_2_TF_FILE = Path(__file__).parent.resolve() / "track-2.tf" TRACK_DEV_TF_FILE = Path(__file__).parent.resolve() / "track-dev.tf" @@ -23,9 +19,7 @@ def test_deploy_from_track_2( # GIVEN a module deployed from track 2 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], timeout=60 * 60) - tls_ctx = get_tls_context(tmp_path, ca_model, "self-signed-certificates") - catalogue_apps_are_reachable(cos_model, tls_ctx) + generic_assertions(cos_model, ca_model, tmp_path) def test_deploy_to_track_dev( @@ -36,6 +30,4 @@ def test_deploy_to_track_dev( tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model) # THEN the model is upgraded and is healthy - wait_for_active_idle_without_error([ca_model, cos_model]) - tls_ctx = get_tls_context(tmp_path, ca_model, "self-signed-certificates") - catalogue_apps_are_reachable(cos_model, tls_ctx) + generic_assertions(cos_model, ca_model, tmp_path) 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 9e49fea8..b5d91db6 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 @@ -7,7 +7,7 @@ from pathlib import Path import jubilant -from helpers import catalogue_apps_are_reachable, wait_for_active_idle_without_error +from helpers import generic_assertions TRACK_2_TF_FILE = Path(__file__).parent.resolve() / "track-2.tf" TRACK_DEV_TF_FILE = Path(__file__).parent.resolve() / "track-dev.tf" @@ -17,8 +17,7 @@ def test_deploy_from_track_2(tf_manager, cos_model: jubilant.Juju): # GIVEN a module deployed from track 2 tf_manager.init(TRACK_2_TF_FILE) tf_manager.apply(model=cos_model.model) - wait_for_active_idle_without_error([cos_model], timeout=60 * 60) - catalogue_apps_are_reachable(cos_model) + generic_assertions(cos_model) def test_deploy_to_track_dev(tf_manager, cos_model: jubilant.Juju): @@ -27,5 +26,4 @@ def test_deploy_to_track_dev(tf_manager, cos_model: jubilant.Juju): tf_manager.apply(model=cos_model.model) # THEN the model is upgraded and is healthy - wait_for_active_idle_without_error([cos_model]) - catalogue_apps_are_reachable(cos_model) + generic_assertions(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 f253fa9b..0d9eae1c 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 @@ -7,7 +7,7 @@ from pathlib import Path import jubilant -from helpers import catalogue_apps_are_reachable, wait_for_active_idle_without_error +from helpers import generic_assertions TRACK_2_TF_FILE = Path(__file__).parent.resolve() / "track-2.tf" TRACK_DEV_TF_FILE = Path(__file__).parent.resolve() / "track-dev.tf" @@ -17,8 +17,7 @@ def test_deploy_from_track_2(tf_manager, cos_model: jubilant.Juju): # GIVEN a module deployed from track 2 tf_manager.init(TRACK_2_TF_FILE) tf_manager.apply(model=cos_model.model) - wait_for_active_idle_without_error([cos_model], timeout=60 * 60) - catalogue_apps_are_reachable(cos_model) + generic_assertions(cos_model) def test_deploy_to_track_dev(tf_manager, cos_model: jubilant.Juju): @@ -27,5 +26,4 @@ def test_deploy_to_track_dev(tf_manager, cos_model: jubilant.Juju): tf_manager.apply(model=cos_model.model) # THEN the model is upgraded and is healthy - wait_for_active_idle_without_error([cos_model]) - catalogue_apps_are_reachable(cos_model) + generic_assertions(cos_model) diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 4be9a10b..6c0f8ac9 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -43,6 +43,21 @@ def destroy(self, **kwargs): subprocess.run(shlex.split(cmd_str), check=True) +def generic_assertions( + cos_model: jubilant.Juju, + ca_model: jubilant.Juju | None = None, + temp_path: Path | None = None, +): + # generic assertions that are shared between products: cos, cos-lite + wait_for_active_idle_without_error([ca_model, cos_model], timeout=60 * 60) + if ca_model: + assert temp_path is not None, "temp_path is required when ca_model is provided" + tls_ctx = get_tls_context(temp_path, ca_model, "self-signed-certificates") + else: + tls_ctx = None + catalogue_apps_are_reachable(cos_model, tls_ctx) + + def wait_for_active_idle_without_error( jujus: List[jubilant.Juju], timeout: int = 60 * 45 ): From f404b45de6734c6fe8d9550c42526f0b9e6da2ba Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Thu, 21 May 2026 08:23:19 -0400 Subject: [PATCH 11/11] chore --- terraform/cos-dev/applications.tf | 14 ++-- terraform/cos-dev/integrations.tf | 38 ++++----- terraform/cos-dev/locals.tf | 2 +- terraform/cos-dev/offers.tf | 28 +++---- .../tests/conditional_ingress.tftest.hcl | 15 ++-- terraform/cos-dev/tests/mesh.tftest.hcl | 46 ++++++----- .../cos-dev/tests/reverse_proxy.tftest.hcl | 48 ++++++----- .../cos-dev/tests/revision_pin.tftest.hcl | 4 +- .../tests/topology_and_storage.tftest.hcl | 14 +--- terraform/cos-dev/variables.tf | 81 +++++++++---------- 10 files changed, 138 insertions(+), 152 deletions(-) diff --git a/terraform/cos-dev/applications.tf b/terraform/cos-dev/applications.tf index 304a3b8e..b69056f4 100644 --- a/terraform/cos-dev/applications.tf +++ b/terraform/cos-dev/applications.tf @@ -36,7 +36,7 @@ module "grafana" { } module "istio_ingress" { - count = var.mesh.enabled ? 1 : 0 + count = var.mesh_enabled ? 1 : 0 source = "git::https://github.com/canonical/istio-ingress-k8s-operator//terraform" app_name = var.istio_ingress.app_name channel = local.channels.istio_ingress @@ -49,7 +49,7 @@ module "istio_ingress" { } module "istio_beacon" { - count = var.mesh.enabled ? 1 : 0 + count = var.mesh_enabled ? 1 : 0 source = "git::https://github.com/canonical/istio-beacon-k8s-operator//terraform" app_name = var.istio_beacon.app_name channel = local.channels.istio_beacon @@ -235,7 +235,7 @@ module "seaweedfs" { } module "ssc" { - count = var.reverse_proxy.enabled ? 1 : 0 + count = var.internal_tls ? 1 : 0 source = "git::https://github.com/canonical/self-signed-certificates-operator//terraform" app_name = var.ssc.app_name channel = local.channels.ssc @@ -366,7 +366,7 @@ module "tempo_worker_metrics_generator" { } module "traefik" { - count = var.reverse_proxy.enabled ? 1 : 0 + count = var.internal_tls ? 1 : 0 source = "git::https://github.com/canonical/traefik-k8s-operator//terraform" app_name = var.traefik.app_name channel = local.channels.traefik @@ -403,7 +403,7 @@ resource "juju_application" "s3_integrator_loki" { count = var.storage_backend == "s3" ? 1 : 0 config = merge({ endpoint = var.s3_endpoint - bucket = var.s3_buckets.loki + bucket = var.loki_bucket credentials = "secret:${juju_secret.loki_s3_credentials[0].secret_id}" }, var.s3_integrator.config) constraints = var.s3_integrator.constraints @@ -442,7 +442,7 @@ resource "juju_application" "s3_integrator_mimir" { count = var.storage_backend == "s3" ? 1 : 0 config = merge({ endpoint = var.s3_endpoint - bucket = var.s3_buckets.mimir + bucket = var.mimir_bucket credentials = "secret:${juju_secret.mimir_s3_credentials[0].secret_id}" }, var.s3_integrator.config) constraints = var.s3_integrator.constraints @@ -481,7 +481,7 @@ resource "juju_application" "s3_integrator_tempo" { count = var.storage_backend == "s3" ? 1 : 0 config = merge({ endpoint = var.s3_endpoint - bucket = var.s3_buckets.tempo + bucket = var.tempo_bucket credentials = "secret:${juju_secret.tempo_s3_credentials[0].secret_id}" }, var.s3_integrator.config) constraints = var.s3_integrator.constraints diff --git a/terraform/cos-dev/integrations.tf b/terraform/cos-dev/integrations.tf index 5aa32815..18947ae2 100644 --- a/terraform/cos-dev/integrations.tf +++ b/terraform/cos-dev/integrations.tf @@ -649,7 +649,7 @@ resource "juju_integration" "ingress" { app_name = module.mimir_coordinator.app_name endpoint = "ingress" } - } : k => v if var.ingress[k] && var.reverse_proxy.enabled + } : k => v if var.ingress[k] && var.internal_tls } model_uuid = var.model_uuid @@ -666,7 +666,7 @@ resource "juju_integration" "ingress" { } resource "juju_integration" "grafana_ingress" { - count = var.ingress.grafana && var.reverse_proxy.enabled ? 1 : 0 + count = var.ingress.grafana && var.internal_tls ? 1 : 0 model_uuid = var.model_uuid @@ -694,7 +694,7 @@ resource "juju_integration" "traefik_route" { app_name = module.tempo_coordinator.app_name endpoint = module.tempo_coordinator.requires.ingress } - } : k => v if var.ingress[k] && var.reverse_proxy.enabled + } : k => v if var.ingress[k] && var.internal_tls } model_uuid = var.model_uuid @@ -729,7 +729,7 @@ resource "juju_integration" "istio_ingress" { app_name = module.mimir_coordinator.app_name endpoint = "ingress" } - } : k => v if var.ingress[k] && var.mesh.enabled + } : k => v if var.ingress[k] && var.mesh_enabled } model_uuid = var.model_uuid @@ -746,7 +746,7 @@ resource "juju_integration" "istio_ingress" { } resource "juju_integration" "grafana_istio_ingress" { - count = var.ingress.grafana && var.mesh.enabled ? 1 : 0 + count = var.ingress.grafana && var.mesh_enabled ? 1 : 0 model_uuid = var.model_uuid @@ -774,7 +774,7 @@ resource "juju_integration" "istio_ingress_route" { app_name = module.tempo_coordinator.app_name endpoint = module.tempo_coordinator.requires.ingress } - } : k => v if var.ingress[k] && var.mesh.enabled + } : k => v if var.ingress[k] && var.mesh_enabled } model_uuid = var.model_uuid @@ -809,7 +809,7 @@ resource "juju_integration" "opentelemetry_collector_mimir_metrics" { # -------------- # Certificate Integrations -------------- resource "juju_integration" "internal_certificates" { - for_each = var.reverse_proxy.enabled ? { + for_each = var.internal_tls ? { alertmanager = { app_name = module.alertmanager.app_name endpoint = module.alertmanager.requires.certificates @@ -854,7 +854,7 @@ resource "juju_integration" "internal_certificates" { } resource "juju_integration" "traefik_receive_ca_certificate" { - count = var.reverse_proxy.enabled ? 1 : 0 + count = var.internal_tls ? 1 : 0 model_uuid = var.model_uuid application { @@ -871,12 +871,10 @@ resource "juju_integration" "traefik_receive_ca_certificate" { # -------------- # Provided by an external CA -------------- resource "juju_integration" "external_traefik_certificates" { - count = local.reverse_proxy_termination ? 1 : 0 + count = local.tls_termination ? 1 : 0 model_uuid = var.model_uuid - application { - offer_url = var.reverse_proxy.cmr_urls.certificates - } + application { offer_url = var.external_certificates_offer_url } application { name = module.traefik[0].app_name @@ -885,13 +883,10 @@ resource "juju_integration" "external_traefik_certificates" { } resource "juju_integration" "external_grafana_ca_cert" { - count = local.reverse_proxy_termination ? 1 : 0 + count = local.tls_termination ? 1 : 0 model_uuid = var.model_uuid - application { - offer_url = var.reverse_proxy.cmr_urls.receive_ca_cert - } - + application { offer_url = var.external_ca_cert_offer_url } application { name = module.grafana.app_name endpoint = module.grafana.requires.receive_ca_cert @@ -899,13 +894,10 @@ resource "juju_integration" "external_grafana_ca_cert" { } resource "juju_integration" "external_otelcol_ca_cert" { - count = local.reverse_proxy_termination ? 1 : 0 + count = local.tls_termination ? 1 : 0 model_uuid = var.model_uuid - application { - offer_url = var.reverse_proxy.cmr_urls.receive_ca_cert - } - + application { offer_url = var.external_ca_cert_offer_url } application { name = module.opentelemetry_collector.app_name endpoint = module.opentelemetry_collector.requires.receive_ca_cert @@ -945,7 +937,7 @@ resource "juju_integration" "traces_and_metrics_correlation" { # -------------- # Service Mesh --------------------- resource "juju_integration" "istio_beacon" { - for_each = var.mesh.enabled ? { + for_each = var.mesh_enabled ? { alertmanager = { app_name = module.alertmanager.app_name endpoint = module.alertmanager.requires.service_mesh diff --git a/terraform/cos-dev/locals.tf b/terraform/cos-dev/locals.tf index 14f75b58..f9f1c3b2 100644 --- a/terraform/cos-dev/locals.tf +++ b/terraform/cos-dev/locals.tf @@ -1,5 +1,5 @@ locals { - reverse_proxy_termination = var.reverse_proxy.enabled && var.reverse_proxy.cmr_urls.certificates != null ? true : false + tls_termination = var.external_certificates_offer_url != null ? true : false traefik_base = "ubuntu@20.04" tracks = { alertmanager = "dev" diff --git a/terraform/cos-dev/offers.tf b/terraform/cos-dev/offers.tf index 7ea4df9d..f925b10a 100644 --- a/terraform/cos-dev/offers.tf +++ b/terraform/cos-dev/offers.tf @@ -38,7 +38,7 @@ resource "juju_offer" "tempo_tracing" { # -------------- # CMR Mesh offers -------------- # resource "juju_offer" "alertmanager_provide_cmr_mesh" { - count = var.mesh.enabled ? 1 : 0 + count = var.mesh_enabled ? 1 : 0 name = "alertmanager-provide-cmr-mesh" model_uuid = var.model_uuid application_name = module.alertmanager.app_name @@ -46,7 +46,7 @@ resource "juju_offer" "alertmanager_provide_cmr_mesh" { } resource "juju_offer" "alertmanager_require_cmr_mesh" { - count = var.mesh.enabled ? 1 : 0 + count = var.mesh_enabled ? 1 : 0 name = "alertmanager-require-cmr-mesh" model_uuid = var.model_uuid application_name = module.alertmanager.app_name @@ -54,7 +54,7 @@ resource "juju_offer" "alertmanager_require_cmr_mesh" { } resource "juju_offer" "catalogue_provide_cmr_mesh" { - count = var.mesh.enabled ? 1 : 0 + count = var.mesh_enabled ? 1 : 0 name = "catalogue-provide-cmr-mesh" model_uuid = var.model_uuid application_name = module.catalogue.app_name @@ -62,7 +62,7 @@ resource "juju_offer" "catalogue_provide_cmr_mesh" { } resource "juju_offer" "catalogue_require_cmr_mesh" { - count = var.mesh.enabled ? 1 : 0 + count = var.mesh_enabled ? 1 : 0 name = "catalogue-require-cmr-mesh" model_uuid = var.model_uuid application_name = module.catalogue.app_name @@ -70,7 +70,7 @@ resource "juju_offer" "catalogue_require_cmr_mesh" { } resource "juju_offer" "grafana_provide_cmr_mesh" { - count = var.mesh.enabled ? 1 : 0 + count = var.mesh_enabled ? 1 : 0 name = "grafana-provide-cmr-mesh" model_uuid = var.model_uuid application_name = module.grafana.app_name @@ -80,7 +80,7 @@ resource "juju_offer" "grafana_provide_cmr_mesh" { } resource "juju_offer" "grafana_require_cmr_mesh" { - count = var.mesh.enabled ? 1 : 0 + count = var.mesh_enabled ? 1 : 0 name = "grafana-require-cmr-mesh" model_uuid = var.model_uuid application_name = module.grafana.app_name @@ -90,7 +90,7 @@ resource "juju_offer" "grafana_require_cmr_mesh" { } resource "juju_offer" "loki_provide_cmr_mesh" { - count = var.mesh.enabled ? 1 : 0 + count = var.mesh_enabled ? 1 : 0 name = "loki-provide-cmr-mesh" model_uuid = var.model_uuid application_name = module.loki_coordinator.app_name @@ -98,7 +98,7 @@ resource "juju_offer" "loki_provide_cmr_mesh" { } resource "juju_offer" "loki_require_cmr_mesh" { - count = var.mesh.enabled ? 1 : 0 + count = var.mesh_enabled ? 1 : 0 name = "loki-require-cmr-mesh" model_uuid = var.model_uuid application_name = module.loki_coordinator.app_name @@ -106,7 +106,7 @@ resource "juju_offer" "loki_require_cmr_mesh" { } resource "juju_offer" "mimir_provide_cmr_mesh" { - count = var.mesh.enabled ? 1 : 0 + count = var.mesh_enabled ? 1 : 0 name = "mimir-provide-cmr-mesh" model_uuid = var.model_uuid application_name = module.mimir_coordinator.app_name @@ -114,7 +114,7 @@ resource "juju_offer" "mimir_provide_cmr_mesh" { } resource "juju_offer" "mimir_require_cmr_mesh" { - count = var.mesh.enabled ? 1 : 0 + count = var.mesh_enabled ? 1 : 0 name = "mimir-require-cmr-mesh" model_uuid = var.model_uuid application_name = module.mimir_coordinator.app_name @@ -122,7 +122,7 @@ resource "juju_offer" "mimir_require_cmr_mesh" { } resource "juju_offer" "opentelemetry_collector_provide_cmr_mesh" { - count = var.mesh.enabled ? 1 : 0 + count = var.mesh_enabled ? 1 : 0 name = "opentelemetry-collector-provide-cmr-mesh" model_uuid = var.model_uuid application_name = module.opentelemetry_collector.app_name @@ -130,7 +130,7 @@ resource "juju_offer" "opentelemetry_collector_provide_cmr_mesh" { } resource "juju_offer" "opentelemetry_collector_require_cmr_mesh" { - count = var.mesh.enabled ? 1 : 0 + count = var.mesh_enabled ? 1 : 0 name = "opentelemetry-collector-require-cmr-mesh" model_uuid = var.model_uuid application_name = module.opentelemetry_collector.app_name @@ -138,7 +138,7 @@ resource "juju_offer" "opentelemetry_collector_require_cmr_mesh" { } resource "juju_offer" "tempo_provide_cmr_mesh" { - count = var.mesh.enabled ? 1 : 0 + count = var.mesh_enabled ? 1 : 0 name = "tempo-provide-cmr-mesh" model_uuid = var.model_uuid application_name = module.tempo_coordinator.app_name @@ -146,7 +146,7 @@ resource "juju_offer" "tempo_provide_cmr_mesh" { } resource "juju_offer" "tempo_require_cmr_mesh" { - count = var.mesh.enabled ? 1 : 0 + count = var.mesh_enabled ? 1 : 0 name = "tempo-require-cmr-mesh" model_uuid = var.model_uuid application_name = module.tempo_coordinator.app_name diff --git a/terraform/cos-dev/tests/conditional_ingress.tftest.hcl b/terraform/cos-dev/tests/conditional_ingress.tftest.hcl index faef5fbd..819bb44a 100644 --- a/terraform/cos-dev/tests/conditional_ingress.tftest.hcl +++ b/terraform/cos-dev/tests/conditional_ingress.tftest.hcl @@ -1,17 +1,14 @@ mock_provider "juju" {} -variables { - model_uuid = "00000000-0000-0000-0000-000000000000" -} +variables { model_uuid = "00000000-0000-0000-0000-000000000000" } -# TODO: This feature also depends on the x2 Traefik story, maybe reverse proxy is not the right name +# TODO: This feature also depends on the x2 Traefik story, maybe internal_tls is not the right name # TODO: Do we need to remove offers / outputs TF conditionally? # TODO: We need to keep the COS API the same across products: feature in COS, COS Lite, and COS Dev -# TODO: For the conditional ingress story, do we support the same components between mesh and reverse proxy? If yes, then they can share the ingress var. Otherwise not, e.g. COS Lite -# --- reverse proxy: enabled - all ingress disabled --- +# --- internal_tls: enabled - all ingress disabled --- -run "reverse_proxy_ingress_disabled" { +run "internal_tls_ingress_disabled" { command = plan variables { @@ -48,8 +45,8 @@ run "mesh_ingress_disabled" { command = plan variables { - mesh = { enabled = true } - reverse_proxy = { enabled = false } + mesh_enabled = true + internal_tls = false ingress = { alertmanager = false catalogue = false diff --git a/terraform/cos-dev/tests/mesh.tftest.hcl b/terraform/cos-dev/tests/mesh.tftest.hcl index 2ceba568..acc0bec3 100644 --- a/terraform/cos-dev/tests/mesh.tftest.hcl +++ b/terraform/cos-dev/tests/mesh.tftest.hcl @@ -1,30 +1,28 @@ mock_provider "juju" {} -variables { - model_uuid = "00000000-0000-0000-0000-000000000000" -} +variables { model_uuid = "00000000-0000-0000-0000-000000000000" } -# --- mesh and reverse_proxy enabled: validation error --- +# --- mesh and internal_tls enabled: validation error --- -run "mesh_and_reverse_proxy_enabled_fails" { +run "mesh_and_internal_tls_enabled_fails" { command = plan variables { - mesh = { enabled = true } - reverse_proxy = { enabled = true } + mesh_enabled = true + internal_tls = true } - expect_failures = [var.mesh.enabled] + expect_failures = [var.mesh_enabled] } -# --- mesh and reverse_proxy disabled: no validation error --- +# --- mesh and internal_tls disabled: no validation error --- -run "mesh_and_reverse_proxy_disabled" { +run "mesh_and_internal_tls_disabled" { command = plan variables { - mesh = { enabled = false } - reverse_proxy = { enabled = false } + mesh_enabled = false + internal_tls = false } } @@ -47,10 +45,15 @@ run "mesh_disabled" { condition = length(juju_integration.istio_beacon) == 0 error_message = "Expected no istio_beacon integrations when the mesh is disabled" } + + assert { + condition = length(juju_integration.istio_ingress) == 0 + error_message = "Expected no istio_ingress integrations when the mesh is disabled" + } assert { condition = length(juju_integration.grafana_istio_ingress) == 0 - error_message = "Expected no grafana_istio_beacon integrations when the mesh is disabled" + error_message = "Expected no grafana_istio_ingress integrations when the mesh is disabled" } assert { @@ -65,8 +68,8 @@ run "mesh_enabled" { command = plan variables { - mesh = { enabled = true } - reverse_proxy = { enabled = false } + mesh_enabled = true + internal_tls = false } assert { @@ -79,19 +82,24 @@ run "mesh_enabled" { error_message = "Expected istio_ingress module when the mesh is enabled" } + assert { + condition = length(juju_integration.istio_beacon) == 7 + error_message = "Unexpected istio_beacon integrations when the mesh is enabled" + } + assert { condition = length(juju_integration.istio_ingress) == 4 - error_message = "Expected 4 istio_ingress integrations (alertmanager, catalogue, loki, mimir), got ${length(juju_integration.istio_ingress)}" + error_message = "Unexpected istio_ingress integrations when the mesh is enabled" } - # Grafana uses a separate count-based resource due to lifecycle replace_triggered_by assert { condition = length(juju_integration.grafana_istio_ingress) == 1 - error_message = "Expected 1 grafana_istio_ingress integration, got ${length(juju_integration.grafana_istio_ingress)}" + error_message = "Unexpected grafana_istio_ingress integrations when the mesh is enabled" } assert { condition = length(juju_integration.istio_ingress_route) == 2 - error_message = "Expected 2 istio_ingress_route integrations (opentelemetry_collector, tempo), got ${length(juju_integration.istio_ingress_route)}" + error_message = "Unexpected istio_ingress_route integrations when the mesh is enabled" + } } diff --git a/terraform/cos-dev/tests/reverse_proxy.tftest.hcl b/terraform/cos-dev/tests/reverse_proxy.tftest.hcl index 357fd464..f6f49d75 100644 --- a/terraform/cos-dev/tests/reverse_proxy.tftest.hcl +++ b/terraform/cos-dev/tests/reverse_proxy.tftest.hcl @@ -1,63 +1,67 @@ mock_provider "juju" {} -variables { - model_uuid = "00000000-0000-0000-0000-000000000000" -} +variables { model_uuid = "00000000-0000-0000-0000-000000000000" } -# --- default: reverse proxy enabled - ingress via traefik --- +# --- default: internal_tls enabled - ingress via traefik --- -run "reverse_proxy_enabled" { +run "internal_tls_enabled" { command = plan + assert { + condition = length(module.ssc) == 1 + error_message = "Expected ssc module when internal_tls is enabled" + } + assert { condition = length(module.traefik) == 1 - error_message = "Expected traefik module when the reverse proxy is enabled" + error_message = "Expected traefik module when internal_tls is enabled" } assert { condition = length(juju_integration.ingress) == 4 - error_message = "Expected 4 ingress integrations (alertmanager, catalogue, loki, mimir), got ${length(juju_integration.ingress)}" + error_message = "Unexpected ingress integrations when internal_tls is enabled" } - # Grafana uses a separate count-based resource due to lifecycle replace_triggered_by assert { condition = length(juju_integration.grafana_ingress) == 1 - error_message = "Expected 1 grafana_ingress integration, got ${length(juju_integration.grafana_ingress)}" + error_message = "Unexpected grafana_ingress integrations when internal_tls is enabled" } assert { condition = length(juju_integration.traefik_route) == 2 - error_message = "Expected 2 traefik_route integrations (opentelemetry_collector, tempo), got ${length(juju_integration.traefik_route)}" + error_message = "Unexpected traefik_route integrations when internal_tls is enabled" } } -# --- reverse proxy disabled: no ingress via traefik --- +# --- internal_tls disabled: no ingress via traefik --- -run "reverse_proxy_disabled" { +run "internal_tls_disabled" { command = plan - variables { - mesh = { enabled = false } - reverse_proxy = { enabled = false } + variables { internal_tls = false } + + assert { + condition = length(module.ssc) == 0 + error_message = "Expected no self-signed-certificates module when internal_tls is disabled" } assert { condition = length(module.traefik) == 0 - error_message = "Expected no traefik module when the reverse proxy is disabled" + error_message = "Expected no traefik module when internal_tls is disabled" } assert { - condition = length(juju_integration.istio_ingress) == 0 - error_message = "Expected 0 istio_ingress integrations when the reverse proxy is disabled, got ${length(juju_integration.istio_ingress)}" + condition = length(juju_integration.ingress) == 0 + error_message = "Unexpected ingress integrations when internal_tls is disabled" } assert { - condition = length(juju_integration.grafana_istio_ingress) == 0 - error_message = "Expected 0 grafana_istio_ingress integrations when the reverse proxy is disabled" + condition = length(juju_integration.grafana_ingress) == 0 + error_message = "Unexpected grafana_ingress integrations when internal_tls is disabled" } assert { - condition = length(juju_integration.istio_ingress_route) == 0 - error_message = "Expected 0 istio_ingress_route integrations when the reverse proxy is disabled" + condition = length(juju_integration.traefik_route) == 0 + error_message = "Unexpected traefik_route integrations when internal_tls is disabled" } } diff --git a/terraform/cos-dev/tests/revision_pin.tftest.hcl b/terraform/cos-dev/tests/revision_pin.tftest.hcl index 7cf5f32e..6e68c9ac 100644 --- a/terraform/cos-dev/tests/revision_pin.tftest.hcl +++ b/terraform/cos-dev/tests/revision_pin.tftest.hcl @@ -1,8 +1,6 @@ mock_provider "juju" {} -variables { - model_uuid = "00000000-0000-0000-0000-000000000000" -} +variables { model_uuid = "00000000-0000-0000-0000-000000000000" } # --- User revision pin is respected and not overridden by juju_charm datasource --- diff --git a/terraform/cos-dev/tests/topology_and_storage.tftest.hcl b/terraform/cos-dev/tests/topology_and_storage.tftest.hcl index e29c3cdf..fd145fd6 100644 --- a/terraform/cos-dev/tests/topology_and_storage.tftest.hcl +++ b/terraform/cos-dev/tests/topology_and_storage.tftest.hcl @@ -1,8 +1,6 @@ mock_provider "juju" {} -variables { - model_uuid = "00000000-0000-0000-0000-000000000000" -} +variables { model_uuid = "00000000-0000-0000-0000-000000000000" } # --- Default: monolithic topology with seaweedfs --- @@ -214,11 +212,6 @@ run "monolithic_s3" { s3_endpoint = "https://s3.example.com" s3_access_key = "access-key" s3_secret_key = "secret-key" - s3_buckets = { - loki = "loki" - mimir = "mimir" - tempo = "tempo" - } } assert { @@ -292,11 +285,6 @@ run "distributed_s3" { s3_endpoint = "https://s3.example.com" s3_access_key = "access-key" s3_secret_key = "secret-key" - s3_buckets = { - loki = "loki" - mimir = "mimir" - tempo = "tempo" - } } assert { diff --git a/terraform/cos-dev/variables.tf b/terraform/cos-dev/variables.tf index 7e466ff9..a54905aa 100644 --- a/terraform/cos-dev/variables.tf +++ b/terraform/cos-dev/variables.tf @@ -48,49 +48,44 @@ variable "storage_backend" { # -------------- # Network configurations -------------- -variable "mesh" { +# TODO: If we name this one _enabled then we should also rename to internal_ca_enabled +variable "mesh_enabled" { description = "Configure the service mesh." - type = object({ - enabled = optional(bool, false) - cmr_urls = optional(object({ - alermanager = optional(string, null) - catalogue = optional(string, null) - grafana = optional(string, null) - loki = optional(string, null) - mimir = optional(string, null) - tempo = optional(string, null) - }), {} - ) - }) - default = {} + type = bool + default = false validation { - condition = !(var.mesh.enabled && var.reverse_proxy.enabled) + condition = !(var.internal_tls && var.mesh_enabled) error_message = "mesh_enabled and internal_tls cannot both be enabled at the same time." } } -variable "reverse_proxy" { - description = "Configure the reverse proxy." - type = object({ - enabled = optional(bool, true) - cmr_urls = optional(object({ - certificates = optional(string, null) - receive_ca_cert = optional(string, null) - }), {} - ) - }) - default = {} +variable "internal_tls" { + description = "Specify whether to use TLS or not for internal COS communication. By default, TLS is enabled using self-signed-certificates" + type = bool + default = true +} + +variable "external_certificates_offer_url" { + description = "A Juju offer URL of a CA providing the 'tls_certificates' integration for Traefik to supply it with server certificates" + type = string + default = null validation { condition = ( - (var.reverse_proxy.cmr_urls.certificates == null && var.reverse_proxy.cmr_urls.receive_ca_cert == null) || - (var.reverse_proxy.cmr_urls.certificates != null && var.reverse_proxy.cmr_urls.receive_ca_cert != null) + (var.external_certificates_offer_url == null && var.external_ca_cert_offer_url == null) || + (var.external_certificates_offer_url != null && var.external_ca_cert_offer_url != null) ) - error_message = "CMRs for certificates and receive_ca_cert must be supplied together (either both set or both null)." + error_message = "external_certificates_offer_url and external_ca_cert_offer_url must be supplied together (either both set or both null)." } } +variable "external_ca_cert_offer_url" { + description = "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." + type = string + default = null +} + # -------------- # Ingress configurations -------------- variable "ingress" { @@ -129,18 +124,22 @@ variable "s3_secret_key" { default = null } -# TODO: Move the refactor efforts into a separate PR that the mesh team can branch from to get mesh merged: -# 1. buckets, reverse_proxy -# 2. I couldn't group s3_ because this has "sensitive" vars. Check if this is 100% not possible. -# What if we made the whole s3 var sensitive? -variable "s3_buckets" { - description = "S3 bucket names for components" - type = object({ - loki = optional(string, "loki") - mimir = optional(string, "mimir") - tempo = optional(string, "tempo") - }) - default = {} +variable "loki_bucket" { + description = "Loki S3 bucket name" + type = string + default = "loki" +} + +variable "mimir_bucket" { + description = "Mimir S3 bucket name" + type = string + default = "mimir" +} + +variable "tempo_bucket" { + description = "Tempo S3 bucket name" + type = string + default = "tempo" } # -------------- # Application configurations --------------