diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index ba75c2f..fd1fd2f 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 +Refresh +============= + +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. Refresh 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/installation/cos-lite-microk8s-sandbox.tf b/docs/tutorial/installation/cos-lite-microk8s-sandbox.tf index c3d1a1b..630acc7 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" } diff --git a/docs/tutorial/refresh-product-module.md b/docs/tutorial/refresh-product-module.md new file mode 100644 index 0000000..c9f9432 --- /dev/null +++ b/docs/tutorial/refresh-product-module.md @@ -0,0 +1,111 @@ +# Refresh COS to a new channel + +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 + +This tutorial assumes that you already: + +- 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 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 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 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`. +``` + +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" +--- +``` + +```{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). +``` + +Finally, add the provider definitions into the same `./main.tf` file: + +```hcl +terraform { + required_providers { + juju = { + source = "juju/juju" + version = "~> 1.0" + } + http = { + source = "hashicorp/http" + version = "~> 3.0" + } + } +} +``` + +At this point, you will have one `main.tf` file ready for deployment. Now you can plan these changes with: + +```shell +terraform plan +``` + +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 +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" { + +# snip ... + + ~ 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. +``` + +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 `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 refresh compatibility: + +- [how-to cross-track upgrade](/how-to/upgrade/) +- [release policy](/reference/release-policy/) +- [release notes](/reference/release-notes/) diff --git a/terraform/charmhub/README.md b/terraform/charmhub/README.md new file mode 100644 index 0000000..e5aa694 --- /dev/null +++ b/terraform/charmhub/README.md @@ -0,0 +1,89 @@ +# 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 | +|------|---------| +| [http](#provider\_http) | ~> 3.0 | + +## Modules + +No modules. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [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 | +|------|-------------| +| [charm\_revision](#output\_charm\_revision) | The revision number for the specified charm channel and base | + + +## 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..956aa9c --- /dev/null +++ b/terraform/charmhub/main.tf @@ -0,0 +1,82 @@ +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 && + + # 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 + ) + ] + + revision = length(local.matching_channels) > 0 ? local.matching_channels[0].revision.revision : null +} + +check "revision_found" { + assert { + condition = local.revision != null + # 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." + } +} + +output "charm_revision" { + description = "The revision number for the specified charm channel and base" + value = local.revision +} diff --git a/terraform/cos-lite/applications.tf b/terraform/cos-lite/applications.tf index dd1bd5a..f1cd520 100644 --- a/terraform/cos-lite/applications.tf +++ b/terraform/cos-lite/applications.tf @@ -5,9 +5,11 @@ module "alertmanager" { 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" { @@ -17,7 +19,7 @@ module "catalogue" { 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 +31,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 +44,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 +56,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..f839cc6 --- /dev/null +++ b/terraform/cos-lite/charmhub.tf @@ -0,0 +1,36 @@ +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" +} + +# 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/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..2d517a3 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. Check Charmhub for per-charm base support." + default = "ubuntu@24.04" + type = string +} + variable "channel" { description = "Channel that the applications are (unless overwritten by external_channels) deployed from" type = string 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