From aa244a2203fca94864c69e2851c629471db5a166 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Thu, 15 Jan 2026 08:55:07 -0500 Subject: [PATCH 1/9] feat: Charmhub module for upgrades --- terraform/charmhub/README.md | 122 +++++++++++++++++++++++++++++++++++ terraform/charmhub/main.tf | 72 +++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 terraform/charmhub/README.md create mode 100644 terraform/charmhub/main.tf diff --git a/terraform/charmhub/README.md b/terraform/charmhub/README.md new file mode 100644 index 0000000..5eb901f --- /dev/null +++ b/terraform/charmhub/README.md @@ -0,0 +1,122 @@ +# Terraform module for the COS solution + +This Terraform module computes a charm’s latest revision (from a channel and base) using the CharmHub API. + + +## Providers + +| Name | Version | +|------|---------| +| [juju](#provider\_juju) | ~> 1.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [alertmanager](#module\_alertmanager) | git:: | n/a | +| [catalogue](#module\_catalogue) | git:: | n/a | +| [grafana](#module\_grafana) | git:: | n/a | +| [loki](#module\_loki) | git:: | n/a | +| [mimir](#module\_mimir) | git:: | n/a | +| [opentelemetry\_collector](#module\_opentelemetry\_collector) | git:: | n/a | +| [ssc](#module\_ssc) | git:: | n/a | +| [tempo](#module\_tempo) | git:: | n/a | +| [traefik](#module\_traefik) | git:: | n/a | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [alertmanager](#input\_alertmanager) | Application configuration for Alertmanager. For more details: |
object({
app_name = optional(string, "alertmanager")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | +| [anti\_affinity](#input\_anti\_affinity) | Enable anti-affinity constraints across all HA modules (Mimir, Loki, Tempo) | `bool` | `true` | no | +| [catalogue](#input\_catalogue) | Application configuration for Catalogue. For more details: |
object({
app_name = optional(string, "catalogue")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | +| [channel](#input\_channel) | Channel that the applications are (unless overwritten by external\_channels) deployed from | `string` | n/a | yes | +| [cloud](#input\_cloud) | Kubernetes cloud or environment where this COS module will be deployed (e.g self-managed, aws) | `string` | `"self-managed"` | no | +| [external\_ca\_cert\_offer\_url](#input\_external\_ca\_cert\_offer\_url) | A Juju offer URL (e.g. admin/external-ca.send-ca-cert) of a CA providing the 'certificate\_transfer' integration for applications to trust ingress via Traefik. | `string` | `null` | no | +| [external\_certificates\_offer\_url](#input\_external\_certificates\_offer\_url) | A Juju offer URL of a CA providing the 'tls\_certificates' integration for Traefik to supply it with server certificates | `string` | `null` | no | +| [grafana](#input\_grafana) | Application configuration for Grafana. For more details: |
object({
app_name = optional(string, "grafana")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | +| [internal\_tls](#input\_internal\_tls) | Specify whether to use TLS or not for internal COS communication. By default, TLS is enabled using self-signed-certificates | `bool` | `true` | no | +| [loki\_bucket](#input\_loki\_bucket) | Loki bucket name | `string` | `"loki"` | no | +| [loki\_coordinator](#input\_loki\_coordinator) | Application configuration for Loki Coordinator. For more details: |
object({
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 3)
})
| `{}` | no | +| [loki\_worker](#input\_loki\_worker) | Application configuration for all Loki Workers. For more details: |
object({
backend_config = optional(map(string), {})
read_config = optional(map(string), {})
write_config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
backend_units = optional(number, 3)
read_units = optional(number, 3)
write_units = optional(number, 3)
})
| `{}` | no | +| [mimir\_bucket](#input\_mimir\_bucket) | Mimir bucket name | `string` | `"mimir"` | no | +| [mimir\_coordinator](#input\_mimir\_coordinator) | Application configuration for Mimir Coordinator. For more details: |
object({
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 3)
})
| `{}` | no | +| [mimir\_worker](#input\_mimir\_worker) | Application configuration for all Mimir Workers. For more details: |
object({
backend_config = optional(map(string), {})
read_config = optional(map(string), {})
write_config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
backend_units = optional(number, 3)
read_units = optional(number, 3)
write_units = optional(number, 3)
})
| `{}` | 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: |
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 | +| [s3\_access\_key](#input\_s3\_access\_key) | S3 access-key credential | `string` | n/a | yes | +| [s3\_endpoint](#input\_s3\_endpoint) | S3 endpoint | `string` | n/a | yes | +| [s3\_integrator](#input\_s3\_integrator) | Application configuration for all S3-integrators in coordinated workers. For more details: |
object({
channel = optional(string, "2/edge")
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 | `string` | n/a | yes | +| [ssc](#input\_ssc) | Application configuration for Self-signed-certificates. For more details: |
object({
app_name = optional(string, "ca")
channel = optional(string, "1/stable")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | +| [tempo\_bucket](#input\_tempo\_bucket) | Tempo bucket name | `string` | `"tempo"` | no | +| [tempo\_coordinator](#input\_tempo\_coordinator) | Application configuration for Tempo Coordinator. For more details: |
object({
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 3)
})
| `{}` | no | +| [tempo\_worker](#input\_tempo\_worker) | Application configuration for all Tempo workers. For more details: |
object({
querier_config = optional(map(string), {})
query_frontend_config = optional(map(string), {})
ingester_config = optional(map(string), {})
distributor_config = optional(map(string), {})
compactor_config = optional(map(string), {})
metrics_generator_config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
compactor_units = optional(number, 3)
distributor_units = optional(number, 3)
ingester_units = optional(number, 3)
metrics_generator_units = optional(number, 3)
querier_units = optional(number, 3)
query_frontend_units = optional(number, 3)
})
| `{}` | no | +| [traefik](#input\_traefik) | Application configuration for Traefik. For more details: |
object({
app_name = optional(string, "traefik")
channel = optional(string, "latest/stable")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [components](#output\_components) | All Terraform charm modules which make up this product module | +| [offers](#output\_offers) | All Juju offers which are exposed by this product module | + + +## Usage + +This example defines and provides multiple charm names to the `charmhubs` module. This module then +computes the latest revision in the specified channel e.g., `2/stable`. Finally, it creates +`juju_application.apps` with the computed revisions. + +```hcl +terraform { + required_providers { + juju = { + source = "juju/juju" + } + http = { + source = "hashicorp/http" + version = "~> 3.0" + } + } +} + +locals { + channel = "2/stable" + base = "ubuntu@24.04" + + charms = { + alertmanager = "alertmanager-k8s" + prometheus = "prometheus-k8s" + grafana = "grafana-k8s" + } +} + +module "charmhubs" { + source = "../charmhub" + for_each = local.charms + + charm = each.value + channel = local.channel + base = local.base + architecture = "amd64" +} + +resource "juju_model" "development" { + name = "development" +} + +resource "juju_application" "apps" { + for_each = local.charms + + model_uuid = juju_model.development.uuid + trust = true + + charm { + name = each.value + channel = local.channel + revision = module.charmhubs[each.key].charm_revision + base = local.base + } +} +``` diff --git a/terraform/charmhub/main.tf b/terraform/charmhub/main.tf new file mode 100644 index 0000000..99aa8f3 --- /dev/null +++ b/terraform/charmhub/main.tf @@ -0,0 +1,72 @@ +terraform { + required_providers { + http = { + source = "hashicorp/http" + version = "~> 3.0" + } + } +} + +variable "charm" { + description = "Name of the charm (e.g., postgresql)" + type = string +} + +variable "channel" { + description = "Channel name (e.g., 14/stable, 16/edge)" + type = string +} + +variable "base" { + description = "Base Ubuntu (e.g., ubuntu@22.04, ubuntu@24.04)" + type = string +} + +variable "architecture" { + description = "Architecture (e.g., amd64, arm64)" + type = string + default = "amd64" +} + +data "http" "charmhub_info" { + url = "https://api.charmhub.io/v2/charms/info/${var.charm}?fields=channel-map.revision.revision" + + request_headers = { + Accept = "application/json" + } + + lifecycle { + postcondition { + condition = self.status_code == 200 + error_message = "Failed to fetch charm info from Charmhub API" + } + } +} + +locals { + charmhub_response = jsondecode(data.http.charmhub_info.response_body) + base_version = split("@", var.base)[1] + + matching_channels = [ + for entry in local.charmhub_response["channel-map"] : + entry if( + entry.channel.name == var.channel && + entry.channel.base.channel == local.base_version && + entry.channel.base.architecture == var.architecture + ) + ] + + revision = length(local.matching_channels) > 0 ? local.matching_channels[0].revision.revision : null +} + +check "revision_found" { + assert { + condition = local.revision != null + error_message = "No matching revision found for charm '${var.charm}' with channel '${var.channel}', base '${var.base}', and architecture '${var.architecture}'. Please verify the combination exists in Charmhub." + } +} + +output "charm_revision" { + description = "The revision number for the specified charm channel and base" + value = local.revision +} From 6a5e1922c9cc772f0325356a7ee7a1c59b96f616 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Thu, 15 Jan 2026 10:26:24 -0500 Subject: [PATCH 2/9] chore: README --- terraform/charmhub/README.md | 47 ++++++------------------------------ 1 file changed, 7 insertions(+), 40 deletions(-) diff --git a/terraform/charmhub/README.md b/terraform/charmhub/README.md index 5eb901f..e5aa694 100644 --- a/terraform/charmhub/README.md +++ b/terraform/charmhub/README.md @@ -7,59 +7,26 @@ This Terraform module computes a charm’s latest revision (from a channel and b | Name | Version | |------|---------| -| [juju](#provider\_juju) | ~> 1.0 | +| [http](#provider\_http) | ~> 3.0 | ## Modules -| Name | Source | Version | -|------|--------|---------| -| [alertmanager](#module\_alertmanager) | git:: | n/a | -| [catalogue](#module\_catalogue) | git:: | n/a | -| [grafana](#module\_grafana) | git:: | n/a | -| [loki](#module\_loki) | git:: | n/a | -| [mimir](#module\_mimir) | git:: | n/a | -| [opentelemetry\_collector](#module\_opentelemetry\_collector) | git:: | n/a | -| [ssc](#module\_ssc) | git:: | n/a | -| [tempo](#module\_tempo) | git:: | n/a | -| [traefik](#module\_traefik) | git:: | n/a | +No modules. ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [alertmanager](#input\_alertmanager) | Application configuration for Alertmanager. For more details: |
object({
app_name = optional(string, "alertmanager")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | -| [anti\_affinity](#input\_anti\_affinity) | Enable anti-affinity constraints across all HA modules (Mimir, Loki, Tempo) | `bool` | `true` | no | -| [catalogue](#input\_catalogue) | Application configuration for Catalogue. For more details: |
object({
app_name = optional(string, "catalogue")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | -| [channel](#input\_channel) | Channel that the applications are (unless overwritten by external\_channels) deployed from | `string` | n/a | yes | -| [cloud](#input\_cloud) | Kubernetes cloud or environment where this COS module will be deployed (e.g self-managed, aws) | `string` | `"self-managed"` | no | -| [external\_ca\_cert\_offer\_url](#input\_external\_ca\_cert\_offer\_url) | A Juju offer URL (e.g. admin/external-ca.send-ca-cert) of a CA providing the 'certificate\_transfer' integration for applications to trust ingress via Traefik. | `string` | `null` | no | -| [external\_certificates\_offer\_url](#input\_external\_certificates\_offer\_url) | A Juju offer URL of a CA providing the 'tls\_certificates' integration for Traefik to supply it with server certificates | `string` | `null` | no | -| [grafana](#input\_grafana) | Application configuration for Grafana. For more details: |
object({
app_name = optional(string, "grafana")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | -| [internal\_tls](#input\_internal\_tls) | Specify whether to use TLS or not for internal COS communication. By default, TLS is enabled using self-signed-certificates | `bool` | `true` | no | -| [loki\_bucket](#input\_loki\_bucket) | Loki bucket name | `string` | `"loki"` | no | -| [loki\_coordinator](#input\_loki\_coordinator) | Application configuration for Loki Coordinator. For more details: |
object({
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 3)
})
| `{}` | no | -| [loki\_worker](#input\_loki\_worker) | Application configuration for all Loki Workers. For more details: |
object({
backend_config = optional(map(string), {})
read_config = optional(map(string), {})
write_config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
backend_units = optional(number, 3)
read_units = optional(number, 3)
write_units = optional(number, 3)
})
| `{}` | no | -| [mimir\_bucket](#input\_mimir\_bucket) | Mimir bucket name | `string` | `"mimir"` | no | -| [mimir\_coordinator](#input\_mimir\_coordinator) | Application configuration for Mimir Coordinator. For more details: |
object({
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 3)
})
| `{}` | no | -| [mimir\_worker](#input\_mimir\_worker) | Application configuration for all Mimir Workers. For more details: |
object({
backend_config = optional(map(string), {})
read_config = optional(map(string), {})
write_config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
backend_units = optional(number, 3)
read_units = optional(number, 3)
write_units = optional(number, 3)
})
| `{}` | 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: |
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 | -| [s3\_access\_key](#input\_s3\_access\_key) | S3 access-key credential | `string` | n/a | yes | -| [s3\_endpoint](#input\_s3\_endpoint) | S3 endpoint | `string` | n/a | yes | -| [s3\_integrator](#input\_s3\_integrator) | Application configuration for all S3-integrators in coordinated workers. For more details: |
object({
channel = optional(string, "2/edge")
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 | `string` | n/a | yes | -| [ssc](#input\_ssc) | Application configuration for Self-signed-certificates. For more details: |
object({
app_name = optional(string, "ca")
channel = optional(string, "1/stable")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | -| [tempo\_bucket](#input\_tempo\_bucket) | Tempo bucket name | `string` | `"tempo"` | no | -| [tempo\_coordinator](#input\_tempo\_coordinator) | Application configuration for Tempo Coordinator. For more details: |
object({
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 3)
})
| `{}` | no | -| [tempo\_worker](#input\_tempo\_worker) | Application configuration for all Tempo workers. For more details: |
object({
querier_config = optional(map(string), {})
query_frontend_config = optional(map(string), {})
ingester_config = optional(map(string), {})
distributor_config = optional(map(string), {})
compactor_config = optional(map(string), {})
metrics_generator_config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
compactor_units = optional(number, 3)
distributor_units = optional(number, 3)
ingester_units = optional(number, 3)
metrics_generator_units = optional(number, 3)
querier_units = optional(number, 3)
query_frontend_units = optional(number, 3)
})
| `{}` | no | -| [traefik](#input\_traefik) | Application configuration for Traefik. For more details: |
object({
app_name = optional(string, "traefik")
channel = optional(string, "latest/stable")
config = optional(map(string), {})
constraints = optional(string, "arch=amd64")
revision = optional(number, null)
storage_directives = optional(map(string), {})
units = optional(number, 1)
})
| `{}` | no | +| [architecture](#input\_architecture) | Architecture (e.g., amd64, arm64) | `string` | `"amd64"` | no | +| [base](#input\_base) | Base Ubuntu (e.g., ubuntu@22.04, ubuntu@24.04) | `string` | n/a | yes | +| [channel](#input\_channel) | Channel name (e.g., 14/stable, 16/edge) | `string` | n/a | yes | +| [charm](#input\_charm) | Name of the charm (e.g., postgresql) | `string` | n/a | yes | ## Outputs | Name | Description | |------|-------------| -| [components](#output\_components) | All Terraform charm modules which make up this product module | -| [offers](#output\_offers) | All Juju offers which are exposed by this product module | +| [charm\_revision](#output\_charm\_revision) | The revision number for the specified charm channel and base | ## Usage From d8b0fbddbba5d505675e65dbf6b75c88cd179bcc Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Thu, 15 Jan 2026 13:35:04 -0500 Subject: [PATCH 3/9] feat: add an upgrade doc --- docs/tutorial/index.rst | 16 ++- .../installation/cos-lite-microk8s-sandbox.md | 2 + docs/tutorial/upgrade-product-module.md | 127 ++++++++++++++++++ 3 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 docs/tutorial/upgrade-product-module.md diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index ba75c2f..a151de9 100644 --- a/docs/tutorial/index.rst +++ b/docs/tutorial/index.rst @@ -19,6 +19,16 @@ or COS Lite, Juju-based observability stacks running on Kubernetes. 1. Deploy the observability stack +Upgrade +============= + +In this part of the tutorial you will learn how to upgrade COS( Lite) to a new channel using Terraform. + +.. toctree:: + :maxdepth: 1 + + 2. Upgrade COS to a new channel + Configuration ============= @@ -29,7 +39,7 @@ charm. .. toctree:: :maxdepth: 1 - 2. Sync alert rules from Git + 3. Sync alert rules from Git Instrumentation =============== @@ -41,7 +51,7 @@ application using the Grafana Agent machine charm. .. toctree:: :maxdepth: 1 - 3. Instrument machine charms + 4. Instrument machine charms Redaction @@ -52,4 +62,4 @@ By implementing a solid redaction strategy you can mitigate the risk of unwanted .. toctree:: :maxdepth: 1 - 4. Redact sensitive data + 5. Redact sensitive data diff --git a/docs/tutorial/installation/cos-lite-microk8s-sandbox.md b/docs/tutorial/installation/cos-lite-microk8s-sandbox.md index e93b23d..5195285 100644 --- a/docs/tutorial/installation/cos-lite-microk8s-sandbox.md +++ b/docs/tutorial/installation/cos-lite-microk8s-sandbox.md @@ -173,6 +173,8 @@ $ juju deploy cos-lite \ --overlay ./storage-small-overlay.yaml ``` +(deploy-cos-ref)= + ## 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: diff --git a/docs/tutorial/upgrade-product-module.md b/docs/tutorial/upgrade-product-module.md new file mode 100644 index 0000000..39ab0d8 --- /dev/null +++ b/docs/tutorial/upgrade-product-module.md @@ -0,0 +1,127 @@ +# Upgrade COS to a new channel + +In this example, you will learn how to deploy COS Lite and upgrade from channel `2/stable` to `2/edge`. To do this, we can deploy COS Lite via Terraform in the same way as [in the tutorial](https://documentation.ubuntu.com/observability/track-2/tutorial/installation/cos-lite-microk8s-sandbox). + +## Prerequisites + +This tutorial assumes that you already have the following: + +- Deployed {ref}`COS Lite with Terraform ` + +## Introduction + +Imagine you have COS Lite (or COS) deployed on a specific channel like `2/stable` and want to +upgrade to a different channel or track e.g., `2/edge`. To do so, an admin would have to manually +`juju refresh` each COS charm, or specify the correct revision in the Terraform module and apply. + +This is simplified with the `charmhubs` module, which allows the juju admin to specify a list of +COS charms to upgrade within the specified `track/channel`. The rest is handled by Terraform. + +## Update the COS Lite Terraform module + +Once deployed, we can add the `locals` definition and a `charmhubs` module: + +```{note} +Copy all the Terraform blocks into one file and remove the `+` symbols. +They are only used to highlight the changes to the COS Lite Terraform module. +``` + +```{literalinclude} /tutorial/installation/cos-lite-microk8s-sandbox.tf +:lines: 1-7 +``` + +```diff ++ http = { ++ source = "hashicorp/http" ++ version = "~> 3.0" ++ } +``` + +```{literalinclude} /tutorial/installation/cos-lite-microk8s-sandbox.tf +:lines: 8-18, 20-21 +``` + +```diff ++ channel = local.channel ++ alertmanager = { revision = module.charmhubs["alertmanager"].charm_revision } ++ catalogue = { revision = module.charmhubs["catalogue"].charm_revision } ++ grafana = { revision = module.charmhubs["grafana"].charm_revision } ++ loki = { revision = module.charmhubs["loki"].charm_revision } ++ prometheus = { revision = module.charmhubs["prometheus"].charm_revision } +} + ++locals { ++ channel = "2/edge" ++ base = "ubuntu@24.04" ++ ++ charms = { ++ alertmanager = "alertmanager-k8s" ++ catalogue = "catalogue-k8s" ++ grafana = "grafana-k8s" ++ loki = "loki-k8s" ++ prometheus = "prometheus-k8s" ++ } ++} + ++module "charmhubs" { ++ source = "../charmhub" ++ for_each = local.charms ++ ++ charm = each.value ++ channel = local.channel ++ base = local.base ++ architecture = "amd64" ++} +``` + +and apply these changes on top of the previous state with: + +```shell +terraform -chdir= apply +``` + +you will notice that Terraform updates each charm to the latest revision in the `2/edge` channel: + +```shell +Terraform used the selected providers to generate the following +execution plan. Resource actions are indicated with the following +symbols: + + create + ~ update in-place + +Terraform will perform the following actions: + + # module.cos.module.alertmanager.juju_application.alertmanager will be updated in-place + ~ resource "juju_application" "alertmanager" { + id = "23dae45b-db71-405b-8035-1bc57a6e6285:alertmanager" + ~ machines = [] -> (known after apply) + name = "alertmanager" + ~ storage = [ + - { + - count = 1 -> null + - label = "data-5" -> null + - pool = "kubernetes" -> null + - size = "1G" -> null + }, + ] -> (known after apply) + # (7 unchanged attributes hidden) + + ~ charm { + ~ channel = "2/stable" -> "2/edge" + name = "alertmanager-k8s" + ~ revision = 191 -> 192 + # (1 unchanged attribute hidden) + } + } + +# snip ... + +Plan: 0 to add, 5 to change, 0 to destroy. +``` + +## Upgrade information + +You can consult the follow release documentation for upgrade compatibility: + +- [release-policy](/reference/release-policy/) +- [release-notes](/reference/release-notes/) From 0634ec8e76c7f88d7086ef5b739791299d47db57 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Fri, 16 Jan 2026 09:05:33 -0500 Subject: [PATCH 4/9] chore: doc improvements --- .../installation/cos-lite-microk8s-sandbox.tf | 5 +- docs/tutorial/upgrade-product-module.md | 101 +++++++++++------- 2 files changed, 64 insertions(+), 42 deletions(-) diff --git a/docs/tutorial/installation/cos-lite-microk8s-sandbox.tf b/docs/tutorial/installation/cos-lite-microk8s-sandbox.tf index c3d1a1b..9432b5c 100644 --- a/docs/tutorial/installation/cos-lite-microk8s-sandbox.tf +++ b/docs/tutorial/installation/cos-lite-microk8s-sandbox.tf @@ -8,6 +8,8 @@ terraform { } } +# before-cos + resource "juju_model" "cos" { name = "cos" config = { logging-config = "=WARNING; unit=DEBUG" } @@ -16,7 +18,8 @@ resource "juju_model" "cos" { 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" ssc = { channel = "1/stable" } traefik = { channel = "latest/edge" } + # before-channel + channel = "2/stable" } diff --git a/docs/tutorial/upgrade-product-module.md b/docs/tutorial/upgrade-product-module.md index 39ab0d8..2ceedf5 100644 --- a/docs/tutorial/upgrade-product-module.md +++ b/docs/tutorial/upgrade-product-module.md @@ -11,34 +11,32 @@ This tutorial assumes that you already have the following: ## Introduction Imagine you have COS Lite (or COS) deployed on a specific channel like `2/stable` and want to -upgrade to a different channel or track e.g., `2/edge`. To do so, an admin would have to manually -`juju refresh` each COS charm, or specify the correct revision in the Terraform module and apply. +upgrade to a different channel (or track) e.g., `2/edge`. To do so, an admin would have to manually +`juju refresh` each COS charm. Or they can determine the correct charm revisions, update the Terraform module, and apply. This is simplified with the `charmhubs` module, which allows the juju admin to specify a list of COS charms to upgrade within the specified `track/channel`. The rest is handled by Terraform. ## Update the COS Lite Terraform module -Once deployed, we can add the `locals` definition and a `charmhubs` module: +Once deployed, we can: + +1. update the `cos-lite` module +2. determine which charms to upgrade +3. add the `locals` and `charmhubs` modules ```{note} -Copy all the Terraform blocks into one file and remove the `+` symbols. -They are only used to highlight the changes to the COS Lite Terraform module. +This tutorial assumed you have deployed COS Lite from a root module located at `./main.tf`. ``` -```{literalinclude} /tutorial/installation/cos-lite-microk8s-sandbox.tf -:lines: 1-7 -``` - -```diff -+ http = { -+ source = "hashicorp/http" -+ version = "~> 3.0" -+ } -``` +First, update your `cos-lite` module, in the existing `./main.tf` file, with the updated content: ```{literalinclude} /tutorial/installation/cos-lite-microk8s-sandbox.tf -:lines: 8-18, 20-21 +--- +language: hcl +start-after: "# before-cos" +end-before: "# before-channel" +--- ``` ```diff @@ -49,38 +47,56 @@ They are only used to highlight the changes to the COS Lite Terraform module. + loki = { revision = module.charmhubs["loki"].charm_revision } + prometheus = { revision = module.charmhubs["prometheus"].charm_revision } } +``` -+locals { -+ channel = "2/edge" -+ base = "ubuntu@24.04" -+ -+ charms = { -+ alertmanager = "alertmanager-k8s" -+ catalogue = "catalogue-k8s" -+ grafana = "grafana-k8s" -+ loki = "loki-k8s" -+ prometheus = "prometheus-k8s" -+ } -+} - -+module "charmhubs" { -+ source = "../charmhub" -+ for_each = local.charms -+ -+ charm = each.value -+ channel = local.channel -+ base = local.base -+ architecture = "amd64" -+} +Then remove the `+` symbols; they are only used to highlight the changes to the `cos-lite` module. +Finally, add the feature components (required for upgrading the product) into the same `./main.tf` file: + +```hcl +terraform { + required_providers { + juju = { + source = "juju/juju" + version = "~> 1.0" + } + http = { + source = "hashicorp/http" + version = "~> 3.0" + } + } +} + +locals { + channel = "2/edge" + base = "ubuntu@24.04" + + charms = { + alertmanager = "alertmanager-k8s" + catalogue = "catalogue-k8s" + grafana = "grafana-k8s" + loki = "loki-k8s" + prometheus = "prometheus-k8s" + } +} + +module "charmhubs" { + source = "../charmhub" + for_each = local.charms + + charm = each.value + channel = local.channel + base = local.base + architecture = "amd64" +} ``` -and apply these changes on top of the previous state with: +At this point, you will have one `main.tf` file. Now you can plan these changes with: ```shell -terraform -chdir= apply +terraform plan ``` -you will notice that Terraform updates each charm to the latest revision in the `2/edge` channel: +you will notice that Terraform plans to update each charm to the latest revision in the `2/edge` channel: ```shell Terraform used the selected providers to generate the following @@ -121,6 +137,9 @@ Plan: 0 to add, 5 to change, 0 to destroy. ## Upgrade information +This tutorial only considers upgrading COS Lite. However, the `charmhubs` module is product-agnostic +and can be used to upgrade charms, and other products e.g., COS. + You can consult the follow release documentation for upgrade compatibility: - [release-policy](/reference/release-policy/) From 2c634d1ec6b2ea664edd8bfe433aeeca8012606a Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Mon, 19 Jan 2026 08:54:29 -0500 Subject: [PATCH 5/9] feat: charmhub inside COS Lite --- docs/tutorial/index.rst | 6 +- ...ct-module.md => refresh-product-module.md} | 62 +++++++++---------- terraform/cos-lite/applications.tf | 18 +++--- terraform/cos-lite/charmhub.tf | 30 +++++++++ terraform/cos-lite/integrations.tf | 9 ++- terraform/cos-lite/variables.tf | 7 +++ 6 files changed, 88 insertions(+), 44 deletions(-) rename docs/tutorial/{upgrade-product-module.md => refresh-product-module.md} (64%) create mode 100644 terraform/cos-lite/charmhub.tf diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index a151de9..fd1fd2f 100644 --- a/docs/tutorial/index.rst +++ b/docs/tutorial/index.rst @@ -19,15 +19,15 @@ or COS Lite, Juju-based observability stacks running on Kubernetes. 1. Deploy the observability stack -Upgrade +Refresh ============= -In this part of the tutorial you will learn how to upgrade COS( Lite) to a new channel using Terraform. +In this part of the tutorial you will learn how to refresh COS (or COS Lite) to a new channel using Terraform. .. toctree:: :maxdepth: 1 - 2. Upgrade COS to a new channel + 2. Refresh COS to a new channel Configuration ============= diff --git a/docs/tutorial/upgrade-product-module.md b/docs/tutorial/refresh-product-module.md similarity index 64% rename from docs/tutorial/upgrade-product-module.md rename to docs/tutorial/refresh-product-module.md index 2ceedf5..a9550da 100644 --- a/docs/tutorial/upgrade-product-module.md +++ b/docs/tutorial/refresh-product-module.md @@ -1,6 +1,6 @@ -# Upgrade COS to a new channel +# Refresh COS to a new channel -In this example, you will learn how to deploy COS Lite and upgrade from channel `2/stable` to `2/edge`. To do this, we can deploy COS Lite via Terraform in the same way as [in the tutorial](https://documentation.ubuntu.com/observability/track-2/tutorial/installation/cos-lite-microk8s-sandbox). +In this example, you will learn how to deploy COS Lite and refresh from channel `2/stable` to `2/edge`. To do this, we can deploy COS Lite via Terraform in the same way as [in the tutorial](https://documentation.ubuntu.com/observability/track-2/tutorial/installation/cos-lite-microk8s-sandbox). ## Prerequisites @@ -11,19 +11,19 @@ This tutorial assumes that you already have the following: ## Introduction Imagine you have COS Lite (or COS) deployed on a specific channel like `2/stable` and want to -upgrade to a different channel (or track) e.g., `2/edge`. To do so, an admin would have to manually +refresh to a different channel (or track) e.g., `2/edge`. To do so, an admin would have to manually `juju refresh` each COS charm. Or they can determine the correct charm revisions, update the Terraform module, and apply. -This is simplified with the `charmhubs` module, which allows the juju admin to specify a list of -COS charms to upgrade within the specified `track/channel`. The rest is handled by Terraform. +This is simplified with the `charmhub` module, which allows the juju admin to specify a list of +COS charms to refresh within the specified `track/channel`. The rest is handled by Terraform. ## Update the COS Lite Terraform module Once deployed, we can: 1. update the `cos-lite` module -2. determine which charms to upgrade -3. add the `locals` and `charmhubs` modules +2. determine which charms to refresh +3. add the `locals` and `charmhub` modules ```{note} This tutorial assumed you have deployed COS Lite from a root module located at `./main.tf`. @@ -41,11 +41,11 @@ end-before: "# before-channel" ```diff + channel = local.channel -+ alertmanager = { revision = module.charmhubs["alertmanager"].charm_revision } -+ catalogue = { revision = module.charmhubs["catalogue"].charm_revision } -+ grafana = { revision = module.charmhubs["grafana"].charm_revision } -+ loki = { revision = module.charmhubs["loki"].charm_revision } -+ prometheus = { revision = module.charmhubs["prometheus"].charm_revision } ++ alertmanager = { revision = module.charmhub["alertmanager"].charm_revision } ++ catalogue = { revision = module.charmhub["catalogue"].charm_revision } ++ grafana = { revision = module.charmhub["grafana"].charm_revision } ++ loki = { revision = module.charmhub["loki"].charm_revision } ++ prometheus = { revision = module.charmhub["prometheus"].charm_revision } } ``` @@ -79,7 +79,7 @@ locals { } } -module "charmhubs" { +module "charmhub" { source = "../charmhub" for_each = local.charms @@ -109,18 +109,8 @@ Terraform will perform the following actions: # module.cos.module.alertmanager.juju_application.alertmanager will be updated in-place ~ resource "juju_application" "alertmanager" { - id = "23dae45b-db71-405b-8035-1bc57a6e6285:alertmanager" - ~ machines = [] -> (known after apply) - name = "alertmanager" - ~ storage = [ - - { - - count = 1 -> null - - label = "data-5" -> null - - pool = "kubernetes" -> null - - size = "1G" -> null - }, - ] -> (known after apply) - # (7 unchanged attributes hidden) + +# snip ... ~ charm { ~ channel = "2/stable" -> "2/edge" @@ -128,19 +118,27 @@ Terraform will perform the following actions: ~ revision = 191 -> 192 # (1 unchanged attribute hidden) } - } # snip ... Plan: 0 to add, 5 to change, 0 to destroy. ``` -## Upgrade information +and finally apply the changes with: + +```shell +terraform apply +``` + +At this point, you will have successfully upgraded COS Lite from `2/stable` to `2/edge`! + +## Refresh information -This tutorial only considers upgrading COS Lite. However, the `charmhubs` module is product-agnostic -and can be used to upgrade charms, and other products e.g., COS. +This tutorial only considers upgrading COS Lite. However, the `charmhub` module is product-agnostic +and can be used to refresh charms, and other products e.g., COS. -You can consult the follow release documentation for upgrade compatibility: +You can consult the follow release documentation for refresh compatibility: -- [release-policy](/reference/release-policy/) -- [release-notes](/reference/release-notes/) +- [how-to cross-track upgrade](/how-to/upgrade/) +- [release policy](/reference/release-policy/) +- [release notes](/reference/release-notes/) diff --git a/terraform/cos-lite/applications.tf b/terraform/cos-lite/applications.tf index dd1bd5a..c752794 100644 --- a/terraform/cos-lite/applications.tf +++ b/terraform/cos-lite/applications.tf @@ -1,23 +1,27 @@ module "alertmanager" { - source = "git::https://github.com/canonical/alertmanager-k8s-operator//terraform" + # source = "git::https://github.com/canonical/alertmanager-k8s-operator//terraform" + source = "../../../alertmanager-k8s-operator/terraform" app_name = var.alertmanager.app_name channel = var.channel config = var.alertmanager.config constraints = var.alertmanager.constraints model_uuid = var.model_uuid - revision = var.alertmanager.revision + revision = local.alertmanager_revision storage_directives = var.alertmanager.storage_directives units = var.alertmanager.units + + # TODO: Add validation or wrap this in a local } module "catalogue" { - source = "git::https://github.com/canonical/catalogue-k8s-operator//terraform" + # source = "git::https://github.com/canonical/catalogue-k8s-operator//terraform" + source = "../../../catalogue-k8s-operator/terraform" app_name = var.catalogue.app_name channel = var.channel config = var.catalogue.config constraints = var.catalogue.constraints model_uuid = var.model_uuid - revision = var.catalogue.revision + revision = local.catalogue_revision storage_directives = var.catalogue.storage_directives units = var.catalogue.units } @@ -29,7 +33,7 @@ module "grafana" { config = var.grafana.config constraints = var.grafana.constraints model_uuid = var.model_uuid - revision = var.grafana.revision + revision = local.grafana_revision storage_directives = var.grafana.storage_directives units = var.grafana.units } @@ -42,7 +46,7 @@ module "loki" { constraints = var.loki.constraints model_uuid = var.model_uuid storage_directives = var.loki.storage_directives - revision = var.loki.revision + revision = local.loki_revision units = var.loki.units } @@ -54,7 +58,7 @@ module "prometheus" { constraints = var.prometheus.constraints model_uuid = var.model_uuid storage_directives = var.prometheus.storage_directives - revision = var.prometheus.revision + revision = local.prometheus_revision units = var.prometheus.units } diff --git a/terraform/cos-lite/charmhub.tf b/terraform/cos-lite/charmhub.tf new file mode 100644 index 0000000..7347069 --- /dev/null +++ b/terraform/cos-lite/charmhub.tf @@ -0,0 +1,30 @@ +locals { + # User input takes priority + alertmanager_revision = var.alertmanager.revision != null ? var.alertmanager.revision : module.charmhub["alertmanager"].charm_revision + catalogue_revision = var.catalogue.revision != null ? var.catalogue.revision : module.charmhub["catalogue"].charm_revision + grafana_revision = var.grafana.revision != null ? var.grafana.revision : module.charmhub["grafana"].charm_revision + loki_revision = var.loki.revision != null ? var.loki.revision : module.charmhub["loki"].charm_revision + prometheus_revision = var.prometheus.revision != null ? var.prometheus.revision : module.charmhub["prometheus"].charm_revision +} + +variable "charms_to_refresh" { + description = "A map of charm names to query from Charmhub." + type = map(string) + default = { + alertmanager = "alertmanager-k8s" + catalogue = "catalogue-k8s" + grafana = "grafana-k8s" + loki = "loki-k8s" + prometheus = "prometheus-k8s" + } +} + +module "charmhub" { + source = "../charmhub" + for_each = var.charms_to_refresh + + charm = each.value + channel = var.channel + base = var.base + architecture = "amd64" +} diff --git a/terraform/cos-lite/integrations.tf b/terraform/cos-lite/integrations.tf index b976f95..20943b1 100644 --- a/terraform/cos-lite/integrations.tf +++ b/terraform/cos-lite/integrations.tf @@ -234,12 +234,17 @@ resource "juju_integration" "catalogue_ingress" { } } +# TODO: Can we make this conditional based on the computed upgrade between Grafana + +# │ Unable to update application resource, got error: updating charm config: cannot upgrade application "grafana" to charm +# │ "ch:amd64/grafana-k8s-172": would break relation "grafana:ingress traefik:ingress" + resource "juju_integration" "grafana_ingress" { model_uuid = var.model_uuid application { name = module.traefik.app_name - endpoint = module.traefik.endpoints.ingress + endpoint = tonumber(local.alertmanager_revision) >= 175 ? module.traefik.endpoints.ingress : module.traefik.endpoints.traefik_route } application { @@ -413,7 +418,7 @@ resource "juju_integration" "external_grafana_ca_cert" { } resource "juju_integration" "external_prom_ca_cert" { - count = local.tls_termination ? 1 : 0 + count = local.tls_termination && tonumber(local.prometheus_revision) >= 276 ? 1 : 0 model_uuid = var.model_uuid application { diff --git a/terraform/cos-lite/variables.tf b/terraform/cos-lite/variables.tf index 49b379e..3bca2be 100644 --- a/terraform/cos-lite/variables.tf +++ b/terraform/cos-lite/variables.tf @@ -10,6 +10,13 @@ locals { tls_termination = var.external_certificates_offer_url != null ? true : false } +# TODO: Discuss how this was missed bc we don't have any base terraform tests. TF plan would catch this error +variable "base" { + description = "The operating system on which to deploy. E.g. ubuntu@22.04. Changing this value for machine charms will trigger a replace by terraform." + default = "ubuntu@24.04" + type = string +} + variable "channel" { description = "Channel that the applications are (unless overwritten by external_channels) deployed from" type = string From 424447acf2da467d3710a19af629a5444f125882 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Mon, 19 Jan 2026 09:49:38 -0500 Subject: [PATCH 6/9] chore: update tutorial --- docs/tutorial/refresh-product-module.md | 27 ++----------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/docs/tutorial/refresh-product-module.md b/docs/tutorial/refresh-product-module.md index a9550da..996ef57 100644 --- a/docs/tutorial/refresh-product-module.md +++ b/docs/tutorial/refresh-product-module.md @@ -40,16 +40,11 @@ end-before: "# before-channel" ``` ```diff -+ channel = local.channel -+ alertmanager = { revision = module.charmhub["alertmanager"].charm_revision } -+ catalogue = { revision = module.charmhub["catalogue"].charm_revision } -+ grafana = { revision = module.charmhub["grafana"].charm_revision } -+ loki = { revision = module.charmhub["loki"].charm_revision } -+ prometheus = { revision = module.charmhub["prometheus"].charm_revision } ++ channel = local.channel } ``` -Then remove the `+` symbols; they are only used to highlight the changes to the `cos-lite` module. +Then remove the `+` symbol; it is only used to highlight the changes to the `cos-lite` module. Finally, add the feature components (required for upgrading the product) into the same `./main.tf` file: ```hcl @@ -69,24 +64,6 @@ terraform { locals { channel = "2/edge" base = "ubuntu@24.04" - - charms = { - alertmanager = "alertmanager-k8s" - catalogue = "catalogue-k8s" - grafana = "grafana-k8s" - loki = "loki-k8s" - prometheus = "prometheus-k8s" - } -} - -module "charmhub" { - source = "../charmhub" - for_each = local.charms - - charm = each.value - channel = local.channel - base = local.base - architecture = "amd64" } ``` From b3b6492157209f07e781128bb51fe1502cc6b89c Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Mon, 19 Jan 2026 10:05:04 -0500 Subject: [PATCH 7/9] chore: revert apps to remote source --- terraform/cos-lite/applications.tf | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/terraform/cos-lite/applications.tf b/terraform/cos-lite/applications.tf index c752794..f1cd520 100644 --- a/terraform/cos-lite/applications.tf +++ b/terraform/cos-lite/applications.tf @@ -1,6 +1,5 @@ module "alertmanager" { - # source = "git::https://github.com/canonical/alertmanager-k8s-operator//terraform" - source = "../../../alertmanager-k8s-operator/terraform" + source = "git::https://github.com/canonical/alertmanager-k8s-operator//terraform" app_name = var.alertmanager.app_name channel = var.channel config = var.alertmanager.config @@ -14,8 +13,7 @@ module "alertmanager" { } module "catalogue" { - # source = "git::https://github.com/canonical/catalogue-k8s-operator//terraform" - source = "../../../catalogue-k8s-operator/terraform" + source = "git::https://github.com/canonical/catalogue-k8s-operator//terraform" app_name = var.catalogue.app_name channel = var.channel config = var.catalogue.config From 91dcf5f2eec916e7ee7882700b08303e156be559 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Wed, 21 Jan 2026 10:11:40 -0500 Subject: [PATCH 8/9] chore --- .../installation/cos-lite-microk8s-sandbox.tf | 3 +- docs/tutorial/refresh-product-module.md | 38 +++++++------------ terraform/cos-lite/variables.tf | 2 +- 3 files changed, 16 insertions(+), 27 deletions(-) diff --git a/docs/tutorial/installation/cos-lite-microk8s-sandbox.tf b/docs/tutorial/installation/cos-lite-microk8s-sandbox.tf index 9432b5c..8e30b15 100644 --- a/docs/tutorial/installation/cos-lite-microk8s-sandbox.tf +++ b/docs/tutorial/installation/cos-lite-microk8s-sandbox.tf @@ -20,6 +20,5 @@ module "cos-lite" { model_uuid = juju_model.cos.uuid ssc = { channel = "1/stable" } traefik = { channel = "latest/edge" } - # before-channel - channel = "2/stable" + channel = "2/stable" } diff --git a/docs/tutorial/refresh-product-module.md b/docs/tutorial/refresh-product-module.md index 996ef57..c9f9432 100644 --- a/docs/tutorial/refresh-product-module.md +++ b/docs/tutorial/refresh-product-module.md @@ -4,48 +4,43 @@ In this example, you will learn how to deploy COS Lite and refresh from channel ## Prerequisites -This tutorial assumes that you already have the following: +This tutorial assumes that you already: -- Deployed {ref}`COS Lite with Terraform ` +- Know how to deploy {ref}`COS Lite with Terraform ` ## Introduction Imagine you have COS Lite (or COS) deployed on a specific channel like `2/stable` and want to refresh to a different channel (or track) e.g., `2/edge`. To do so, an admin would have to manually -`juju refresh` each COS charm. Or they can determine the correct charm revisions, update the Terraform module, and apply. +`juju refresh` each COS charm and address any refresh errors. Alternatively, they can determine the +correct charm `channel` and `revision`(s), update the Terraform module, and apply. -This is simplified with the `charmhub` module, which allows the juju admin to specify a list of -COS charms to refresh within the specified `track/channel`. The rest is handled by Terraform. +This is simplified within COS (and COS Lite) by mimicking the `juju refresh` behavior on a product +level, allowing the juju admin to specify a list of charms to refresh within the specified +`track/channel`. The rest is handled by Terraform. ## Update the COS Lite Terraform module -Once deployed, we can: - -1. update the `cos-lite` module -2. determine which charms to refresh -3. add the `locals` and `charmhub` modules +Once deployed, we can determine which charms to refresh with the `charms_to_refresh` input variable, detailed in the [README](https://github.com/canonical/observability-stack/tree/main/terraform/cos-lite). This defaults to: all charms owned by the `observability-team`. ```{note} This tutorial assumed you have deployed COS Lite from a root module located at `./main.tf`. ``` -First, update your `cos-lite` module, in the existing `./main.tf` file, with the updated content: +Then, replace `2/stable` with `2/edge` in your `cos-lite` module within the existing `./main.tf` file: ```{literalinclude} /tutorial/installation/cos-lite-microk8s-sandbox.tf --- language: hcl start-after: "# before-cos" -end-before: "# before-channel" --- ``` -```diff -+ channel = local.channel -} +```{note} +The `base` input variable for the `cos-lite` module is important if the `track/channel` deploys charms to a different base than the default, detailed in the [README](https://github.com/canonical/observability-stack/tree/main/terraform/cos-lite). ``` -Then remove the `+` symbol; it is only used to highlight the changes to the `cos-lite` module. -Finally, add the feature components (required for upgrading the product) into the same `./main.tf` file: +Finally, add the provider definitions into the same `./main.tf` file: ```hcl terraform { @@ -60,20 +55,15 @@ terraform { } } } - -locals { - channel = "2/edge" - base = "ubuntu@24.04" -} ``` -At this point, you will have one `main.tf` file. Now you can plan these changes with: +At this point, you will have one `main.tf` file ready for deployment. Now you can plan these changes with: ```shell terraform plan ``` -you will notice that Terraform plans to update each charm to the latest revision in the `2/edge` channel: +and Terraform plans to update each charm to the latest revision in the `2/edge` channel: ```shell Terraform used the selected providers to generate the following diff --git a/terraform/cos-lite/variables.tf b/terraform/cos-lite/variables.tf index 3bca2be..2d517a3 100644 --- a/terraform/cos-lite/variables.tf +++ b/terraform/cos-lite/variables.tf @@ -12,7 +12,7 @@ locals { # TODO: Discuss how this was missed bc we don't have any base terraform tests. TF plan would catch this error variable "base" { - description = "The operating system on which to deploy. E.g. ubuntu@22.04. Changing this value for machine charms will trigger a replace by terraform." + description = "The operating system on which to deploy. E.g. ubuntu@22.04. Changing this value for machine charms will trigger a replace by terraform. Check Charmhub for per-charm base support." default = "ubuntu@24.04" type = string } From 188e4f57d1d1caefd8005cc249b7311e5282983e Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Mon, 9 Feb 2026 08:03:33 -0500 Subject: [PATCH 9/9] chore: dump ideas --- .../installation/cos-lite-microk8s-sandbox.tf | 2 +- terraform/charmhub/main.tf | 16 +++++++++++++--- terraform/cos-lite/charmhub.tf | 6 ++++++ terraform/cos-lite/versions.tf | 1 + 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/tutorial/installation/cos-lite-microk8s-sandbox.tf b/docs/tutorial/installation/cos-lite-microk8s-sandbox.tf index 8e30b15..630acc7 100644 --- a/docs/tutorial/installation/cos-lite-microk8s-sandbox.tf +++ b/docs/tutorial/installation/cos-lite-microk8s-sandbox.tf @@ -18,7 +18,7 @@ resource "juju_model" "cos" { 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" ssc = { channel = "1/stable" } traefik = { channel = "latest/edge" } - channel = "2/stable" } diff --git a/terraform/charmhub/main.tf b/terraform/charmhub/main.tf index 99aa8f3..956aa9c 100644 --- a/terraform/charmhub/main.tf +++ b/terraform/charmhub/main.tf @@ -45,13 +45,21 @@ data "http" "charmhub_info" { locals { charmhub_response = jsondecode(data.http.charmhub_info.response_body) - base_version = split("@", var.base)[1] + # base_version = split("@", var.base)[1] matching_channels = [ for entry in local.charmhub_response["channel-map"] : entry if( entry.channel.name == var.channel && - entry.channel.base.channel == local.base_version && + + # TODO: I think we can ignore this base input if we assume that 24.04 is always dev/and track/2 + # TODO: Capture all matching JSON bodies for channel & architecture. Then validate that it's only one. If not, the user should be warned that the base needs to be specified. + # E.g. you specify channel as 1/stable, but then base defaults to 24.04. This would fail bc 22.04 is for 1/stable + + # TODO: Test that this works with the product to charm channel mapping like the revisions override I have + # curl "https://api.charmhub.io/v2/charms/info/alertmanager-k8s?fields=channel-map.revision.revision" | jq -r '.["channel-map"] + + # entry.channel.base.channel == local.base_version && entry.channel.base.architecture == var.architecture ) ] @@ -62,7 +70,9 @@ locals { check "revision_found" { assert { condition = local.revision != null - error_message = "No matching revision found for charm '${var.charm}' with channel '${var.channel}', base '${var.base}', and architecture '${var.architecture}'. Please verify the combination exists in Charmhub." + # TODO: Undo + # error_message = "No matching revision found for charm '${var.charm}' with channel '${var.channel}', base '${var.base}', and architecture '${var.architecture}'. Please verify the combination exists in Charmhub." + error_message = "No matching revision found for charm '${var.charm}' with channel '${var.channel}', and architecture '${var.architecture}'. Please verify the combination exists in Charmhub." } } diff --git a/terraform/cos-lite/charmhub.tf b/terraform/cos-lite/charmhub.tf index 7347069..f839cc6 100644 --- a/terraform/cos-lite/charmhub.tf +++ b/terraform/cos-lite/charmhub.tf @@ -28,3 +28,9 @@ module "charmhub" { base = var.base architecture = "amd64" } + +# TODO: Remove +output "charm_revisions" { + description = "The revision number for the specified charm channel and base" + value = { for k, v in module.charmhub : k => v.charm_revision } +} \ No newline at end of file diff --git a/terraform/cos-lite/versions.tf b/terraform/cos-lite/versions.tf index f65a349..bac56e0 100644 --- a/terraform/cos-lite/versions.tf +++ b/terraform/cos-lite/versions.tf @@ -5,5 +5,6 @@ terraform { source = "juju/juju" version = "~> 1.0" } + # TODO: Add the http provider here } } \ No newline at end of file