From ecf860d981c4189ee99bb86353d6f3cd3758178c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 16:07:34 +0000 Subject: [PATCH] chore: update Copilot collections to v0.20.0 --- .copilot-collections.yaml | 2 +- .github/agents/charm-engineer.agent.md | 128 +++++ .../documentation.instructions.md | 4 +- .../instructions/secret-guard.instructions.md | 17 + .../skills/terraform-cc008-migration/SKILL.md | 319 ++++++++++ .../assets/cc008.spec.md | 544 ++++++++++++++++++ .../scripts/check_cc008.sh | 42 ++ 7 files changed, 1053 insertions(+), 3 deletions(-) create mode 100644 .github/agents/charm-engineer.agent.md create mode 100644 .github/instructions/secret-guard.instructions.md create mode 100644 .github/skills/terraform-cc008-migration/SKILL.md create mode 100644 .github/skills/terraform-cc008-migration/assets/cc008.spec.md create mode 100755 .github/skills/terraform-cc008-migration/scripts/check_cc008.sh diff --git a/.copilot-collections.yaml b/.copilot-collections.yaml index 72f0e961f6..6c76041d75 100644 --- a/.copilot-collections.yaml +++ b/.copilot-collections.yaml @@ -3,7 +3,7 @@ copilot: # The version of the toolkit to use (matches a Release Tag in this repo) - version: "v0.5.0" + version: "v0.20.0" # The collections you want to install collections: - charm-python diff --git a/.github/agents/charm-engineer.agent.md b/.github/agents/charm-engineer.agent.md new file mode 100644 index 0000000000..8b080928cf --- /dev/null +++ b/.github/agents/charm-engineer.agent.md @@ -0,0 +1,128 @@ +--- +name: charm-engineer +description: Senior software engineer specialized in writing Juju charms +license: Apache-2.0 +metadata.version: 0.1.0 +metadata.author: platform-engineering +--- + +You are a senior software engineer with a strong background in python and in site reliability engineering specialized in writing Juju charms. + +You bring your expertise to create new charm or review existing ones. + +## Actions + +- You MUST start by reading the "Charm implementation guidelines" section. +- You MUST download and analyze all links in this document. + +### When asked for review + +- Take each element of the implementation guidelines. +- Carefully analyze the reviewed charm to see if entirely follows the guideline. +- Report: + + - Guidelines that are fully implemented. + - Guidelines that are partially or not implemented. + - Guidelines that are excluded (= guidelines that are not implemented where there's a comment explaining why). + +### When asked for charm creation + +- Create the charm based on the best practices and the implementation guidelines. +- Look at external resources to get a good understanding of the workload and to get the best practices related to its operation. + +## Charm implementation guidelines + +### Principles + +- Charms are not designed for Canonical only. They should not contain Canonical internal references. +- Charms should be trustworthy. To achieve it: + + - We make their behaviour transparent, reliable and predicable. + + - All charms must use the [holistic pattern](https://documentation.ubuntu.com/ops/latest/explanation/holistic-vs-delta-charms/) (you MUST read this doc). + - Charm must not use `defered` events. + - The "Charm Runtime State Abstraction" principle is applied: + + - Configuration and integration data provided by Juju are abstracted in an internal Pydantic model that is easier to interact with. + - The charm state should implement a `from_charm` method for initialisation which accepts the charm as a generic `CharmBase` argument and may accept additional arguments such as instances of library handlers and the secret storage. + +### Substrate + +By default, we develop K8s charms. A machine charm should only be chosen if the application meets one of the following exception criteria: + +- Low-Level System Access: The application requires specialized features restricted within a Kubernetes environment, such as direct kernel access or raw networking. +- Infrastructure Dependencies: The application serves as a direct dependency for other machine charms. +- Early-Stage Bootstrapping: The application is required during the early phases of datacenter provisioning, meaning it must run before the Kubernetes cluster itself is operational. +- Storage Constraints: The application relies strictly on local storage. + +### Files layout and content + +The base content is described in [Files](https://documentation.ubuntu.com/charmcraft/latest/reference/files/) (you MUST read this doc), and by default we expect: + +- `charm.py` contains the charm code. +- `state.py` contains the runtime state of the charm. For complex charms, we would have a "state/" python module. The purpose is to model the business logic so that we can operate the workload without refering to Juju primitives. +- `workload.py` contains the workload specific operations (include `pebble` functions). It should not refer to any Juju concepts, the operations should go through the state model. + +#### `charm.py` + +- All methods are private and should start with `_`, including `_reconcile`. +- Required ports must explicitely opened with `open_port` or `set_ports`. It's usually an anomaly if no ports are open. + +##### `_reconcile` + +###### Purpose + +The `_reconcile` should be "guarding" the execution of the rest of the code: + +- It evaluates the state, calls the business logic and set the unit status. +- It runs pre-checks ensuring all conditions are met to run the charm properly. +- It exits early if not all pre-checks are met (typically if some required relations are missing). +- It may or may not stop the workload service depending on the workload type: in any case, it should not create production incidents (e.g. "not stopping a load-balancer if one relation is missing"). +- All hooks must be mapped to `_reconcile` but refresh events. +- Everything is part of `_reconcile` but `refresh` events. +- `install` is part of `_reconcile` and should be idempotent + + - `snap install` is ok as it will not trigger an upgrade. + - `apt install` is not ok as it will trigger and upgrade (so the code should first check for the presence of the package) + +###### Implementation + +- The method should be easy to read and let the developper capture the excecution workflow. +- It should delegate as much as it can. +- It should excplicitely call methods within `try/except` blocks (no `decorator` pattern). +- `try/except` blocks should be small and only catch custom exceptions. +- For "multi-modes" charm, the "routing" mode should be identified early, and call specific `_reconcile_` methods. + +A typical `_reconcile` structure is: + +1. Ensure pre-conditions (guarding, exit early, defensive programming) +2. Manipulate / treat relation data, configuration, gather workload status +3. Map the charmstate +4. Branch on the mode, or delegate to services +5. Plan the service / pebble +6. Reload / Restart if necessary +7. Adjust status + +##### Relations + +- Relations should use the `save` and `load` methods to dump and restore data from the relation through Pydantic models. + +#### `rockcraft.yaml` + +- `level=alive` must not be used (see [manage-pebble-health-checks](https://documentation.ubuntu.com/ops/latest/howto/manage-containers/manage-pebble-health-checks/#check-health-endpoint-and-probes) (you MUST read this doc) + +#### `workload.py` + +- DO + + - Only restart workload when the change cannot be applied with a hot reload. + - Restart or reload only once per hook. + +- DON'T + + - Restart workload when a hot reload is available and the changes can be hot reloaded. + - Don't restart/reload multiple times in the same hook. + +#### Jinja2 templates + +Keep rendering logic in charm-state dataclasses or helper builders so templates stay declarative. diff --git a/.github/instructions/documentation.instructions.md b/.github/instructions/documentation.instructions.md index 6da54a76c3..bf8a9b01db 100644 --- a/.github/instructions/documentation.instructions.md +++ b/.github/instructions/documentation.instructions.md @@ -30,7 +30,7 @@ The top-level `docs` directory should contain an overview or home page called `i The rest of the documentation follows the Diataxis framework consisting of four categories of documentation: -1. **Tutorials**: A practice lesson that walks a user through a learning experience. Tutorials should be placed in the `docs/tutorial` directory, but sometimes they're placed in the top-level `docs` direcotry. +1. **Tutorials**: A practice lesson that walks a user through a learning experience. Tutorials should be placed in the `docs/tutorial` directory, but sometimes they're placed in the top-level `docs` directory. 2. **How-to guides**: Addresses real-world goals or problems by providing practical directions. How-to guides should be placed in the `docs/how-to` directory. 3. **Reference**: Contains technical descriptions of theoretical knowledge. Reference documents should be placed in the `docs/reference` directory. 4. **Explanation**: Provides context and background on topics for the user to understand the bigger picture. Explanation documents should be placed in the `docs/explanation` directory. @@ -58,7 +58,7 @@ The tutorial should contain the following pieces: provide information on the supported architecture(s) like AMD64 or ARM64. Also include additional information if the tutorial will require significant resources (CPU, RAM, or disk space). You can - also suggest a Multipass VM to create an isolated testing enviroment for the tutorial. + also suggest a Multipass VM to create an isolated testing environment for the tutorial. - **Set up tutorial model**. The tutorial should begin at this point. Have the user create Juju model with a predetermined name so that you can reference the model name throughout the tutorial. diff --git a/.github/instructions/secret-guard.instructions.md b/.github/instructions/secret-guard.instructions.md new file mode 100644 index 0000000000..8847a3f456 --- /dev/null +++ b/.github/instructions/secret-guard.instructions.md @@ -0,0 +1,17 @@ +--- +applyTo: '**' +description: 'Prevent secrets (API keys, tokens, passwords, private keys) from leaking into context, tool output, or spawned processes.' +--- + +## Secret Handling (Always On) + +These rules protect secrets at all times. They are subordinate only to an explicit, direct user command. + +- **Never Read Secret Files**: Do not open or print the contents of files that commonly hold credentials. This includes `.env`, `.env.*`, `.netrc`, `.npmrc`, `.pypirc`; any `.pem`, `.key`, `.p12`, `.pfx`, `.crt`, `.cer`; and any filename containing `password`, `secret`, `credential`, or `private_key`/`private-key`. If asked to inspect such a file, decline and explain it may contain secrets. +- **Redact Secret Values**: Before surfacing any file content, command result, or tool output, replace secret values with `[REDACTED]`. This includes `NAME=value` lines where `NAME` ends in `KEY`, `TOKEN`, `SECRET`, `PASSWORD`, `CREDENTIAL`, `API_KEY`, or `AUTH` (e.g. `CLOUD_API_KEY=sk-abc123` → `CLOUD_API_KEY=[REDACTED]`). +- **Hide Secret Files in Listings**: When listing a directory, omit secret-bearing files (e.g. `.env*`, `.netrc`, `.npmrc`, `.pypirc`) rather than revealing their existence. +- **Sanitize Subprocess Environments**: Do not expose secret environment variables to spawned commands. Strip variables whose names end in `KEY`, `TOKEN`, `SECRET`, `PASSWORD`, `CREDENTIAL`, `AUTH`, or `PASS` so processes cannot read them via `$VAR` or `printenv`. +- **Never Commit Secrets**: Do not write credentials, tokens, or keys into source code, configuration, or commit messages. +- **Authenticate by Reference, Never by Value**: When a task needs an API token, MCP token, password, or key, do not read, request, or handle the value. Pass a credential *reference* (a name) and let a trusted broker resolve it from the OS keyring and inject it into the request. If no keyring entry exists, ask the user to store it themselves (e.g. `keyring set agent-secrets `) — never accept a pasted secret into the conversation. + +For the full procedure and a keyring broker implementation, use the `secret-guard` skill. diff --git a/.github/skills/terraform-cc008-migration/SKILL.md b/.github/skills/terraform-cc008-migration/SKILL.md new file mode 100644 index 0000000000..c57fad8bb3 --- /dev/null +++ b/.github/skills/terraform-cc008-migration/SKILL.md @@ -0,0 +1,319 @@ +--- +name: terraform-cc008-migration +description: Migrates Terraform charm and product modules to the CC008 Charm Terraform Standards, enforcing the required file layout, variable and output contracts, MAJOR_VERSION markers, module tests and the operator-workflows reusable CI workflows. Use whenever a repository's Terraform modules must be brought to (or audited against) CC008. +metadata: + author: canonical/platform-engineering + version: "1.0.0" +--- + +# Terraform CC008 Migration + +## Overview + +Bring every Terraform module in a repository up to the **CC008 — Charm Terraform +Standards** specification, and wire the repository to the compliance, test and +release automation provided by +[canonical/operator-workflows](https://github.com/canonical/operator-workflows/tree/main/terraform-compliance). + +## When To Use + +- A repository ships one or more Terraform modules that predate CC008 (CC006-era + layout, `versions.tf`, combined `endpoints` output, unpinned module sources). +- A new module must be authored so that it is CC008-compliant from the start. +- A CC008 compliance check fails in CI and the module needs to be corrected. + +## Source Of Truth + +[`assets/cc008.spec.md`](assets/cc008.spec.md) **is** the specification. Read the +sections relevant to the modules you are migrating *before* editing anything, and +resolve every question against it. This skill does not restate the spec; it only +reinforces the parts that are most often missed and describes the repository +plumbing the spec does not cover. + +### Fetching The Companion Files + +This skill ships two companion files next to `SKILL.md`: `assets/cc008.spec.md` +and `scripts/check_cc008.sh`. When the skill is installed as a directory — for +example synced to `.github/skills/terraform-cc008-migration/` — they are already +on disk and the relative paths above resolve. + +When only `SKILL.md` was fetched (`copilot skill add ` materializes a single +file), download them first and use the downloaded copies wherever this document +refers to them: + +```bash +CC008_BASE="https://raw.githubusercontent.com/canonical/copilot-collections/main/groups/platform-engineering/skills/terraform-cc008-migration" +mkdir -p /tmp/cc008 +curl -fsSL -o /tmp/cc008/cc008.spec.md "$CC008_BASE/assets/cc008.spec.md" +curl -fsSL -o /tmp/cc008/check_cc008.sh "$CC008_BASE/scripts/check_cc008.sh" +chmod +x /tmp/cc008/check_cc008.sh +``` + +Delete `/tmp/cc008` once the migration is verified; it must never be committed. + +### Secondary References + +In order of authority when they disagree: + +1. [platform-engineering-charm-template/terraform](https://github.com/canonical/platform-engineering-charm-template/tree/main/terraform) + — the canonical, always-up-to-date reference module. Prefer copying its exact + patterns. +2. [operator-workflows/terraform-compliance](https://github.com/canonical/operator-workflows/tree/main/terraform-compliance) + — the checker that decides whether the migration passed. +3. Finished migrations: + [gateway-api-integrator-operator#320](https://github.com/canonical/gateway-api-integrator-operator/pull/320/files), + [mailserver-operators#48](https://github.com/canonical/mailserver-operators/pull/48/files). + +## Module Discovery And Classification + +Treat every directory containing a `main.tf` (or an existing `versions.tf` / +`terraform.tf`) as one module, whether the repository has a single `terraform/` +directory or several — per-charm `/terraform`, product modules under +`terraform/` or `terraform-product/`. Enumerate them all before +starting: + +```bash +find . -type f -name 'main.tf' -not -path '*/.terraform/*' -exec dirname {} \; | sort -u +``` + +**Then classify each module before applying any rule.** The categories have +*different* input and output contracts, and applying the wrong one is the most +common migration mistake: + +| Category | What it deploys | Key outputs | +| --- | --- | --- | +| **Charm module** | a single charm | `application`, plus `provides` / `requires` | +| **Product module** | a ready-to-use solution, incl. models and integrations | `models`, `metadata` | +| **Deployment** | one specific environment | n/a — versions state and `backend.tf` | + +CC008 also defines *component modules* (several charms sharing one release +cycle). We have none yet, so this skill carries no rules for them — read the +"Component modules" section of the spec directly if you ever meet one. + +The universal rules below apply to every category; then follow **only** the +section matching the category you classified. + +## DO — Every Module + +- **DO** ensure each module has `terraform.tf`, `variables.tf`, `outputs.tf`, + `main.tf` and `README.md`. Rename a legacy `versions.tf` to `terraform.tf`, + leaving the content unchanged except where the next rule requires otherwise. +- **DO** require, in `terraform.tf`, a Terraform `required_version` and a + `juju/juju` provider version that admits `>= 1.0.0` (for example `~> 1.0`, or + `> 1.0.0, < 2.0.0`). +- **DO** order `variable` blocks in `variables.tf` and `output` blocks in + `outputs.tf` alphabetically by name. +- **DO** set `nullable = false` on every variable that must always resolve to a + concrete value (`app_name`, `channel`, `model_uuid`, …) so callers cannot pass + an explicit `null` and bypass the default. Leave variables that intentionally + default to `null` (`base`, `constraints`, `revision`, …) nullable. +- **DO** give every output a `description`. +- **DO** add a `terraform/MAJOR_VERSION` file at the root of each independent + module family, containing only the current major version number and no trailing + newline (start at `1` for a first migration). In multi-module repositories, + make additional modules' `MAJOR_VERSION` files a relative symlink to that root + file **only** when those modules share the same release train (the + mailserver-operators#48 pattern); otherwise give each independently tagged + module family its own file. +- **DO** add a `tests/main.tftest.hcl` per module, using `mock_provider "juju"` + and asserting the module's key outputs, when the module has no test yet. +- **DO** pin every Terraform module `source = "git::...//terraform..."` reference + — in README examples and in product modules — to a `?ref=` tag + or commit hash such as `?ref=tf-1.0.0`. Floating references such as branches + are **not allowed**. + +## DO — Charm Modules + +- **DO** declare the mandatory variables: `app_name`, `channel`, `config`, + `constraints`, `model_uuid` (no default) and `revision`. Add `units` unless the + charm is a subordinate charm, which **must** omit it. Determine this + deterministically — never guess from the charm's name or description: read + the module's own `charmcraft.yaml` (walk up from the `terraform/` directory + to the charm root if it lives elsewhere, e.g. `../charmcraft.yaml` or + `../../charmcraft.yaml`) and check its top-level `subordinate:` key. + `subordinate: true` means omit `units`; anything else (including the key + being absent, which defaults to `false`) means `units` is mandatory. The + compliance checker treats `units` as optional either way, so it will not + catch a wrong call — getting the classification right is this skill's + responsibility, not the checker's. +- **DO** add the optional CC008 variables when they are relevant to the charm: + `base`, `expose`, `resources`, `machines`, `endpoint_bindings`, + `storage_directives`, `offered_endpoints`. +- **DO** output `application` as the `juju_application` resource object itself — + `value = juju_application.` — not its `.name`. +- **DO** output `provides` and `requires` as `map(object({...}))`, one key per + relation endpoint the charm actually declares, each entry carrying at least + `kind = "endpoint"`, `name = juju_application..name` and + `endpoint = ""`. Add `controller = null` when the + relation supports cross-model integration. They are mandatory as soon as the + charm declares endpoints of that kind; use `value = {}` only when it declares + none. +- **DO** replace any deprecated combined `endpoint` / `endpoints` output with the + `provides` / `requires` split. + +## DO — Product Modules + +A product module deploys a ready-to-use solution and owns the `juju_model`, +secret and integration resources tying its charms together. It does **not** +declare `requires`: everything the product needs from the outside is supplied +through input variables, and everything it offers to the outside is exposed +through `offers`. + +- **DO** express every external requirement — a database, TLS, an existing + ingress, a COS stack — as an input variable carrying the endpoint or offer to + integrate with, rather than as a `requires` output for a caller to wire up. +- **DO** take `proxy` and `logging-config` as mandatory inputs whenever the + module creates or manages its own `juju_model` resources, plus `risk` to + control the channel risk of the bundled components. +- **DO** expose the charm revision **and** the OCI resources of every bundled + charm as input variables, so deployments are reproducible and air-gap capable. +- **DO** provide a default implementation for mandatory external integrations + (database, TLS) behind `count = 0`, so it is dropped when the user supplies + their own endpoint or offer through the corresponding variable. +- **DO** output `models`, mapping each model key to its `model_uuid` and the + components deployed in it, and `metadata` carrying at least `version`, + `deployed_at` and `updated_at`. Both are mandatory. +- **DO** output `offers` and, where the solution issues them, `credentials`. + +## DO — Deployments + +- **DO** add `backend.tf` with the backend configuration, and version every file + describing the deployment **including** the state. +- **DO** pin every referenced charm and product module to a tag or commit hash. + +## DO — CI Workflows + +Use operator-workflows' reusable workflows instead of hand-written scripts. + +- **DO** add or update `.github/workflows/terraform_modules_release.yaml` calling + `canonical/operator-workflows/.github/workflows/terraform_modules_release.yaml`, + triggered on push to `main` and on pull requests touching `terraform/**` (plus + any other module paths), with `permissions: contents: write`. +- **DO** add `.github/workflows/terraform_modules_compliance.yaml` calling + `canonical/operator-workflows/.github/workflows/terraform_modules_compliance.yaml`, + triggered on pull requests touching `**/terraform/**`, with a + `terraform-directories` input listing every discovered module directory. +- **DO** update `.github/workflows/test_terraform_modules.yaml` to call + `canonical/operator-workflows/.github/workflows/terraform_modules_test.yaml` + with `terraform-directories` listing every discovered module directory, and + trigger it on pull requests touching `**/terraform/**`. +- **DO** add `.github/workflows/generate_terraform_docs.yaml` calling + `canonical/operator-workflows/.github/workflows/generate_terraform_docs.yaml`. + Trigger it on pushes to `main` that touch `**/terraform/**` or this workflow + file, with these caller permissions so it can create the documentation pull + request: + + ```yaml + permissions: + contents: write + pull-requests: write + ``` +- **DO** pass every discovered module directory as a comma-separated + `terraform-directory` input to the docs workflow. Do not rely on its default + of `terraform` when modules live elsewhere. Ensure each module's + `README.md` contains `` and `` + markers for the generated content. +- **DO** leave the docs workflow's `auto-merge` input unset to retain its + default of `true`. It generates the README changes and opens or updates a + `terraform-docs` pull request after the push to `main`. +- **DO** add each of these three workflow files to *its own* `paths` filter, on + every trigger it declares: + + ```yaml + on: + pull_request: + paths: + - '**/terraform/**' + - '.github/workflows/terraform_modules_compliance.yaml' + ``` + + Without this, bumping the pinned reusable-workflow SHA does not run the new + checks on the pull request that bumps it, so a breaking change in + operator-workflows lands unverified. +- **DO** pin every new — and every pre-existing unpinned — reusable-workflow call + to a commit SHA. Reuse the SHA already used elsewhere in the repository for + `canonical/operator-workflows` if one exists; otherwise resolve the latest + `main` commit yourself and pin to it with a `# main` comment, as + platform-engineering-charm-template does: + + ```bash + git ls-remote https://github.com/canonical/operator-workflows.git main + ``` + + Apply this SHA-pinning requirement to the Terraform docs workflow as well. + +## DO — Repository Housekeeping + +- **DO** add `**/.terraform/` and `**/.terraform.lock.hcl` to the top-level + `.gitignore` if they are not already ignored. +- **DO** add `**/MAJOR_VERSION` to the `header.ignore` list in `.licenserc.yaml` + so the version markers are exempt from license headers. +- **DO** add a changelog entry — in `docs/changelog.md`, or a new file under + `docs/release-notes/artifacts/` if the repository uses that convention — + describing the CC008 migration and any breaking default change (for example + `expose` now defaulting to `{}` instead of `null`). + +## DON'T + +- **DON'T** change any module's deployment behaviour — resource arguments, + variable defaults — beyond what CC008 requires. Preserve existing defaults + unless the spec mandates a different one. +- **DON'T** apply the charm-module contract to a product module — no + `app_name`/`channel`/`revision` variables, no `application` output, and no + `provides` / `requires`. +- **DON'T** touch charm source code, `charmcraft.yaml`, or workflows unrelated to + the Terraform modules. +- **DON'T** hand-roll a tagging or release workflow; call the canonical reusable + workflow instead. +- **DON'T** leave `@main` or any floating branch or tag in a reusable-workflow + call in the final diff, not even as a placeholder. +- **DON'T** commit the scratch directory used by the compliance checker, or any + `.terraform/` artefact produced while validating. +- **DON'T** declare the migration finished on inspection alone — the compliance + checker is the arbiter. + +## Validate Before Declaring Done + +Do not guess at compliance. Run the checker against every discovered module and +iterate until it passes clean: + +```bash +/scripts/check_cc008.sh +``` + +`` is wherever this skill is installed — typically +`.github/skills/terraform-cc008-migration/`. If you downloaded the companion +files instead, run `/tmp/cc008/check_cc008.sh`. + +Called with no arguments the script discovers every module directory itself; pass +explicit directories to narrow the run: + +```bash +/scripts/check_cc008.sh terraform charms/foo/terraform +``` + +It downloads the checker from operator-workflows into a temporary directory and +removes it afterwards, so nothing is left behind to commit. + +Then run the repository's Terraform quality gates: + +```bash +tflint --init && tflint --recursive +terraform fmt -recursive -check +terraform -chdir= init -backend=false && terraform -chdir= test +``` + +## Quality Bar + +- The compliance checker passes for every discovered module directory. +- Every module was classified before editing, and follows the contract of its own + category only. +- `tflint --recursive` and `terraform fmt -recursive -check` are clean. +- `terraform test` passes for every module. +- Every reusable-workflow call is pinned to a commit SHA, and every terraform + workflow lists its own file in its `paths` filter. +- The Terraform docs workflow passes all discovered module directories, + targets README files with the Terraform docs markers, and retains the default + auto-merge behavior. +- The diff contains no scratch directory, no `.terraform/` artefact and no + behavioural change beyond what CC008 requires. diff --git a/.github/skills/terraform-cc008-migration/assets/cc008.spec.md b/.github/skills/terraform-cc008-migration/assets/cc008.spec.md new file mode 100644 index 0000000000..59e8f9b849 --- /dev/null +++ b/.github/skills/terraform-cc008-migration/assets/cc008.spec.md @@ -0,0 +1,544 @@ + +| Index | CC008 | | | +| :---- | :---- | :---- | :---- | +| Title | Charm Terraform Standards | | | +| [**Type**](https://docs.google.com/document/d/1lStJjBGW7lyojgBhxGLUNnliUocYWjAZ1VEbbVduX54/edit?usp=sharing) | **Author(s)** | [**Status**](https://docs.google.com/document/d/1lStJjBGW7lyojgBhxGLUNnliUocYWjAZ1VEbbVduX54/edit?usp=sharing) | **Created** | +| Implementation | [Enrico Deusebio](mailto:enrico.deusebio@canonical.com) | Drafting | Nov 14, 2025 | +| | **Reviewer(s)** | **Status** | **Date** | +| | [Jon Seager](mailto:jon.seager@canonical.com) | Approved | Feb 12, 2026 | +| | [Mykola Marzhan](mailto:mykola.marzhan@canonical.com) | Pending Review | Date | +| | [Grégory Schiano Lomoriello](mailto:gregory.schiano@canonical.com) | Pending Review | Date | +| | [Simon Aronsson](mailto:simon.aronsson@canonical.com) | Approved | Feb 18, 2026 | +| | [Konstantinos Tsakalozos](mailto:kos.tsakalozos@canonical.com) | Pending Review | Date | +| | [Billy Olsen](mailto:billy.olsen@canonical.com) | Pending Review | Date | +| | [Alessandro Cabbia](mailto:alessandro.cabbia@canonical.com) | Pending Review | Date | +| | [Sinan Awad](mailto:sinan.awad@canonical.com) | Pending Review | Date | +| | [Ben Hoyt](mailto:ben.hoyt@canonical.com) | Pending Review | Date | +| | [Dmitry Dmitriev](mailto:dmitry.dmitriev@canonical.com) | Pending Review | Date | +| | [Stefano Liboni](mailto:stefano.liboni@canonical.com) | Pending Review | Date | + +# + +# Abstract + +Terraform modules are used in combination with the Terraform Juju provider to deploy charms and complete solutions in a consistent and reproducible manner. Moreover, with the sunset and deprecation of YAML bundle, Terraform modules are becoming a common artifact implemented by the various charming teams. The current specification defines the standards on how terraform modules are structured, organized and how they can integrate one another. + +# Rationale + +With the deprecation of YAML bundle, every team has been developing terraform modules for their solutions in the past few months/years. These modules are generally used both to provide users an easy way to deploy but also as input for various testing pipelines. SolQA in particular has been developing a new framework for testing solutions based on Terraform (see [SQ065 - Terraform Module On-boarding Requirements for Testing](https://docs.google.com/document/d/1JaJp-RTOxP1zqCMZqgQdSXI8coBuiqhFqmt5OPWjNNM/edit?tab=t.0)). Field and Managed Solution have also been creating deployments based on terraform, by either importing charm team terraform solutions or developing adhoc solutions to meet certain requirements. + +Terraform can allow us to create a framework to re-use well-written, opinionated and well-supported components that can be integrated into full-scale solutions. But for this, it is critical that the terraform modules coming from different teams provide a consistent structure and interface, such that the various components can be integrated easily with one another. Consistency of interfaces and structure is also critical to allow writing high-level testing pipelines that can easily adapt to all various configurations/integrations, therefore allowing testing onboarding on centralized infrastructure (provided by SolQA) as light and fast as possible. + +In the past few years, there has been a rise of the number of modules, that however do not present consistent user-experience and interface, often without leveraging on the lesson learned by the various teams. There has been some attempts to provide more standardization (e.g. see [CC006 - Terraform modules organization in the Juju ecosystem](https://docs.google.com/document/d/1EG71A2pJ244PQRaGVzGj7Mx2B_bzE4U_OSqx4eeVI1E/edit?tab=t.0), [SQ065 - Terraform Module On-boarding Requirements for Testing](https://docs.google.com/document/d/1JaJp-RTOxP1zqCMZqgQdSXI8coBuiqhFqmt5OPWjNNM/edit?tab=t.0) and [DA219 - Data Platform Terraform Modules Structure](https://docs.google.com/document/d/1CWMzbXUxVfMHxGthWNmm8VNsBZKjyi2jRoBS4QWbkIo/edit?tab=t.0)) but these attempts are either not general or have been consolidated and approved. Similarly, there has been an attempt to progress also via hands-on, most notably via the Terraform Mini-sprint in London (see the [Github repo](https://github.com/canonical/terraform-minisprint-terragrunt/tree/main) and the presentation [Solution Deployments With Terraform & Terragrunt](https://docs.google.com/presentation/d/1mFQzi2asJAIrfj7zkXhNUvUa88bbiGt7nCN8JCu6MHY/edit?slide=id.g344d9fe6fa5_0_43#slide=id.g344d9fe6fa5_0_43)). + +By leveraging on the previous work, the current specification aims therefore at filling this gap providing a standard and guidelines on how to structure TF modules. Although still considering the use and integration using Terragrunt, the current specification does not aim at providing a standard ALSO for Terragrunt units and stacks, that will be developed in the future. + +# TLDR: Main differences with respect to CC006 + +* [Splitting endpoints output into provides/requires](https://docs.google.com/document/d/1k1psLCfcf0Nr5KeVtWGFi9wmzhcg5AP9GVZTsRyVQSQ/edit?disco=AAABwd1MJi4) +* [Adding the concept of Component Modules](#component-modules), generalizing concepts from [DA219 - Data Platform Terraform Modules Structure](https://docs.google.com/document/d/1CWMzbXUxVfMHxGthWNmm8VNsBZKjyi2jRoBS4QWbkIo/edit?tab=t.0) +* Requirement of referencing to dependant modules in Product Modules and Component modules by pinning tags or commit hash ([ref](https://docs.google.com/document/d/1k1psLCfcf0Nr5KeVtWGFi9wmzhcg5AP9GVZTsRyVQSQ/edit?disco=AAABwwt33AQ) and [ref](https://docs.google.com/document/d/1k1psLCfcf0Nr5KeVtWGFi9wmzhcg5AP9GVZTsRyVQSQ/edit?disco=AAABwwt33AM)) +* Semantic versioning for [Charm modules](?tab=t.0#heading=h.j1hupdmdkbi6) and [Component modules](?tab=t.0#heading=h.1vskya29blir) and guidance on how to define major,minor and patch bumps +* [Framework for exposing integrations in-model and out-of-model](https://docs.google.com/document/d/1k1psLCfcf0Nr5KeVtWGFi9wmzhcg5AP9GVZTsRyVQSQ/edit?disco=AAABwwt33A4) +* More detailed guidance for [inputs](#input) and [outputs](#output) for Product Modules (ref), integrating concepts from [SQ065 - Terraform Module On-boarding Requirements for Testing](https://docs.google.com/document/d/1JaJp-RTOxP1zqCMZqgQdSXI8coBuiqhFqmt5OPWjNNM/edit?tab=t.0) +* [Release cycle for Product modules](#release-cycle-and-versioning) +* [Concept of Deployment](#deployments) + +# Specification + +**The current specification assumes that the Terraform provider version used is “\>1.0.0”.** + +**The current specification only aims at defining the structure and the standards for writing terraform modules in a consistent manner to facilitate integrations and automations. Tools for implementing linting, checks, CI or also platforms to store terraform modules (e.g. Terraform Registry) are out-of-scope for the current spec. In particular, Terraform Registry implementation is the scope of a currently open spec [CC007 - Terraform module registry](https://docs.google.com/document/d/1vtPeLnLrJ0rfqtLe9_ElvveeOrZp0cFJYD0zrgZ-Fd0/edit?usp=drive_open&ouid=106637444762362243511).** + +## General Design + +### **Terraform module classification** + +Depending on the scope, terraform modules are classified into the following categories: + +* Charm modules, that provides a terraform module for a single charm +* Component modules, that provides a terraform module for a number of charms that are tightly connected and share the same release cycle +* Product (or Solution) modules, that provides a terraform module for ready-to-use solutions that can be deployed with limited configuration. Product modules also contain the definition of the various resources required to tie components and charm modules together (e.g. juju models, juju secrets, integrations, etc) +* Deployment (Deployments), that describes a specific deployment in a specific environment, providing all information to make sure the setup is fully reproducible Deployments are defined and created during the implementation phase by either Field or Managed Solution, and they are then handed over and generally owned by the teams managing the deployments, e.g. Managed Solution, support or customers. SolQA tests may also be seen as a particular instance of a deployment, that however is not handed over, but rather torn down after the test is completed. However, as such, needs to version the various artifacts that can make the deployment reproducible. + +The following figure exemplifies how the various hierarchical categories interact one another +![][image1] + +### **Module structure** + +Every module, irrespectively of their category, should provide a common file structure, that is consistent with the [official styleguide](https://developer.hashicorp.com/terraform/language/style#file-names): + +* README.md \-- Documentation for the Terraform module. The README should clarify at the very least inputs and outputs for the Terraform module. +* (only for deployments) backend.tf \- that contains the backend configurations +* providers.tf \- that contains all provider blocks and configurations +* terraform.tf \- that contains a single `terraform` block that defines the `required_version` and the `required_providers`. The minimum required provider version for Juju is above 1.0.0. +* main.tf \-- The main module Terraform code, that bundle the various modules and resources either created by the Terraform module or data blocks consumed by the Terraform module. If the module ends up being too large, the content can be split into separate files, where resources are bundled by type + * (optional) applications.tf + * (optional) integrations.tf + * (optional) offers.tf + * … +* variables.tf \-- The input variables supported by the module, in alphabetical order +* outputs.tf \-- The outputs of the Terraform module, in alphabetical order. +* locals.tf – The variables used by the Terraform module, in alphabetical order. + +Providing a single structure will be key to allow to create templates, automations, linting and common CIs that can be used across all the terraform modules. Further defining the tools and automations to be used is however out-of-scope for the current spec. + +For Charm modules, Component modules and Product modules these are the only files that need to be versioned. Product modules may also version the input variable files (\`tfvars.json\`) that have been tested and validated. Deployments must version all files describing the deployment as well as the state. The definitions of the \`.gitignore\` file is up to various teams/repositories, although re-using \`.gitignore\` files that are being used in other projects is advised. + +\`\` + +Given that some Terraform registry (e.g. Terrareg) only support versions of the form “{major}.{minor}.{patch}”, tags and versions should not include further information, such as metadata, pre-release info or additional suffixes (e.g. rc, risks, etc). + +In order to publish the terraform module into the Terraform registry, a stable release that features a Terraform module among its artifacts should be available. If the terraform module is still in development, the module may still be used in components or solutions, but referenced using the repository with the commit hash. + +Care must be taken when updating the major, minor and patch version depending on the scope of the change: + +* Major updates needs to be used for changes that may not be backward compatible and require major changes of the deployments, like: + * Changing the name of the resources (that triggers destruction and re-creation of applications) + * Changing default values of config options (e.g. if channel name is provided, upgrading channels that may not be backward compatible) + * Renaming variables or outputs +* Minor updates need to be used for adding new features/functionalities, like: + * Adding more optional configuration options + * Adding new outputs +* Patch updates should be used for trivial fixes, like: + * Updating descriptions + * Non-breaking bug fixes + +### **Module Lifecycle** + +Charm modules, Component modules and Product modules should have their own lifecycle, decoupled from other artifacts, charm, snap and rock releases. Where the source code is stored is up to the various teams. Some examples are: + +1. terraform module lives together with the charms +2. all terraform modules are in a single repository +3. different repositories for each terraform module. + +When terraform modules live together with other terraform modules or charms, git tagging can be used to separate releases of each artifact and allow terraform modules to have their own lifecycle. In the case of a.), we suggest teams to structure the tag with `tf-X.Y.Z` to identify different terraform module releases. In the case of b.), we suggest teams to use `-X.Y.Z` (i.e., `prometheus-k8s-1.1.2).`In case of c.), the general guidance is to use \`vX.Y.Z\`. + +## Charm modules + +When single charms can be used as-is in Solution and Deployments, a dedicated terraform module should be provided to allow their consumption. **If a charm is not to be consumed standalone, providing a Terraform module is not mandatory.** + +In some cases, you may bundle a number of charms that share the same release lifecycle together, as for instance when considering a database \<\> backend \<\> frontend solution. In this case, the database will have its own lifecycle (and it may be provided a single charm module), while backend and frontend may share the same lifecycle, with either the backend and frontend that won't be consumed standalone. In this case, you can bundle the two charms (backend and frontend) in a component module (see next section), without the need of creating a separate charm module for each, and then bundling them into a component module. + +### Inputs {#inputs} + +The following standard variables shall be provided: + +#### Mandatory + +The following variables shall be made available by the module. They can have default values to provide a minimal working deployment. They shall follow this naming convention: + +| Name | Type | Default Value | Description | +| :---- | :---- | :---- | :---- | +| app\_name | string | Charm specific, non null | Name to give the deployed application | +| channel | string | Charm specific, non null | Channel of the charm. If using null as default value, the field should not be nullable. | +| config | map(string) | {} | Map for config options | +| constraints | string | null | String listing constraints for this application | +| model\_uuid | string | | Reference to an existing model uuid. **This field is not Nullable.** | +| revision | number | null | Revision number of the charm. The value must be taken to null to deploy latest revision on a given channel. | +| units | number | 1 | Unit count/scale (except for subordinate charms, where this **must not** be provided) | + +#### Optional + +The following variables can be made available by the module. If they are, they need to follow the defined naming convention, and have a type that matches the one required as input of the \`juju\_application\` resource type (see the [terraform provider documentation](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application#schema) for more information): + +| Name | Type | Default Value | Description | +| :---- | :---- | :---- | :---- | +| base | string | null | The operating system on which to deploy. E.g. ubuntu@22.04. | +| expose | Block List | {} | The *expose* block, as defined in [the documentation](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application#expose-1) | +| resources | map(string) | {} | Resources to use | +| machines | set(string) | \[\] | List of juju\_machine resources to use for deployment | +| endpoint\_bindings | Attribute Set | {} | Map for endpoint bindings | +| storage\_directives | map(string) | {} | Map of storage used by the application | +| offered\_endpoints | list(string) | \[\] | A list of endpoints that are exposed as offers for ease of consumption | + +Other inputs are allowed by this specification. + +### **Outputs** + +Charm modules shall output the name of the deployed application, and the name of all integration endpoints. + +#### Mandatory + +| Name | Type | Description | +| :---- | :---- | :---- | +| application | object | Object representing the deployed application | + + + +#### Optional + +Any other outputs are allowed. The following table covers naming conventions that shall be used if present. + +| Name | Type | Description | +| :---- | :---- | :---- | +| offers | map(object) | Map of all offers exposed by the single charm. The key of the map should be compatible with the one provided in endpoints | +| provides | map(object) | Provides endpoints. **Mandatory if charm has `provides` defined.** | +| requires | map(object) | Requires endpoints. **Mandatory if charm has `requires` defined.** | + +#### Example + +| output "application" {␋ value \= juju\_application.\␋}␋␋output "provides" {␋ value \= {␋ metrics \= { kind \= "endpoint" name \= juju\_application.name endpoint \= "prometheus-metrics" }␋ }␋}␋␋output "requires" {␋ value \= {␋ mimir\_cluster \= { kind \= "endpoint" name \= juju\_application.name endpoint \= "mimir-cluster" }␋ }␋}␋␋output "offers" {␋ value \= {␋ metrics \= { kind \= "offer" url \= \ }␋} | +| :---- | + +## Component modules {#component-modules} + +Component modules are bundles of charms that are required to be deployed together and that they share the same release lifecycle and overall versioning. Therefore, the charms included in the bundle are owned by a single team that is responsible for their maintenance and support. Component modules may or may not include specific charm modules, but if they do, they must ensure that the single charm module and the component module share the same release cycle and versioning information. When referring to charm modules, the reference should use either tags or commit hash to ensure reproducibility. **Floating references (e.g. branches) are not allowed.** + +Component modules may NOT work out of the box, but they may be dependent on other modules to be fully working. Examples of these may be KFP that depends on Kubeflow core services, but also Kyuubi that requires a PostgreSQL database to be used as backend. These should be handled as external components/charm modules to be integrated with. + +Component modules may be provided either on separate standalone repositories or in the same repository depending whether all the charms included in the component already belong to a multi-charm repository and/or terraform modules are generally bundled together. When provided in a multi-charm repository, the terraform module should live under the folder \`\/terraform\`, similarly to what is done for charm modules. The file structure underneath should follow the structure as outlined above. + +Similarly to Charm modules, once stable, components modules can be released to a terraform registry. + +Component modules are generally cloud agnostic (can be used on different clouds), but they target a specific substrate only (e.g. K8s or VM). + +### **Inputs** + +#### Mandatory + +The only mandatory inputs of component modules are listed here: + +| Name | Type | Default Value | Description | +| :---- | :---- | :---- | :---- | +| model\_uuid | string | null | Reference to an existing model resource or data source for the model to deploy to. This field is not Nullable. | +| \ | object | Per field, see below | Object exposing the variables of a specific charm, compatible with the format of charm modules. See [here](#inputs) for further informations | + +The object for each charm module must contain at a minimum the channel, base and revision fields. The goal of these mandatory fields is to allow reproducible deployments of the product. An example charm module input might look like this: + +| variable "\" {␋ type \= object({␋ app\_name \= optional(string, "amf")␋ channel \= optional(string, "1.6/edge")␋ config \= optional(map(string), {})␋ constraints \= optional(string, "arch=amd64")␋ resources \= optional(map(string), {})␋ revision \= optional(number)␋ base \= optional(string, "ubuntu@24.04")␋ units \= optional(number, 1\)␋ })␋} | +| :---- | + +“\” is the name of the \`juju\_application\` resource or the name of the \`module\` being referenced. + +| variable "charm1" {␋ type \= object({␋ … })␋} variable "charm2" {␋ type \= object({␋ … })␋} resource "juju\_application" "charm1" { … } module "charm2" { source="..." } | +| :---- | + +The name of modules/juju application resources may or may not correspond to the name of the charm, and this is left to the charm authors to choose the name that makes more sense. + +#### Optional + +| Name | Type | Default Value | Description | +| :---- | :---- | :---- | :---- | +| \ | object (see structure below) | {} | Nested structure for specifying the external integration that needs to be consumed by the component | +| expose\_endpoints | list(string) | \[\] | A list of endpoints that are exposed as offers for ease of consumption | + +The structure for external\_integrations is of the form + +| variable "ingress" {␋ description \= "Pointers to the dashboard links" type \= object({ kind \= string name \= optional(string, null) endpoint \= optional(string, null) url \= optional(string, null) controller \= optional(strig, null) }) } | +| :---- | + +Such that both cross-model integrations (using offers, and therefore the \`url\` field) and in-model integrations can be supported by the component module. Ideally, this can then be used when creating the \`juju\_integration\` resource + +| resource "juju\_integration" "ingress" {␋ application {␋ name \= var.ingress.kind \== "endpoint"? [var.ingress.name](http://var.ingress.name) : null␋ endpoint \= var.ingress.kind \== "endpoint"? var.ingress.endpoint : null␋ offer\_url \= var.ingress.kind \== "offer"? [var.ingress.](http://var.ingress.name)url : null␋ }␋} | +| :---- | + +If only a specific type of integration (either in-model or cross-model integration) is expected, the signature can be simplified to only support one use-case by also adding a suffix (“offer” and “endpoint”) to explicitly notify which use-case (in-model or cross-model) is supported: + +| variable "ingress\_offer" {␋ description \= "URL to the ingress offer" type \= string } ␋variable "ingress\_endpoint" {␋ description \= "Pointers to the ingress endpoint" type \= object({ name \= optional(string, null) endpoint \= optional(string, null) }) } | +| :---- | + +Note that offers cannot be used for in-model relations, as advised against this pattern from the Juju team ([reference](https://matrix.to/#/!xzmWHtGpPfVCXKivIh:ubuntu.com/$6TSZC9PvBB309k0S3m_cQmKCV3quZ2OBWUjNzAmcXlo?via=ubuntu.com&via=matrix.org&via=xentonix.net)). + +### **Outputs** + +The output for Components module is similar to the one of Module modules, with the only caveat that it extends the format to allow multiple entries: + +#### Mandatory + +| Name | Type | Description | +| :---- | :---- | :---- | +| components | map(object) | Map associating the resource names with the object representing the deployed applications | + +#### Optional + +| Name | Type | Description | +| :---- | :---- | :---- | +| offers | map(object) | Map of all offers exposed by the single charm. The key of the map should be compatible with the one provided in endpoints | +| provides | map(object) | Map of the provides endpoints, with a structure \\_\. **Mandatory if charms have `provides` defined.** | +| requires | map(object) | Map of the requires endpoints, with a structure \\_\. **Mandatory if charms have `requires` defined.** | + +#### Example + +| output "components" {␋ value \= { charm1 \= juju\_application.charm1 charm2 \= module.charm2.application … }␋}␋␋output "provides" {␋ description \= "Map of all the provided endpoints"␋ value \= { charm1\_metrics={ kind: "endpoint" name: juju\_application[.charm1.name](http://.charm1.name) endpoint: "metrics" } charm2\_metrics={ kind: "endpoint" name: module.charm2.app\_name endpoint: module.charm2.provides\["metrics"\] } }␋}␋␋output "requires" {␋ description \= "Map of the required endpoints"␋ …␋ (similar structure to provides)␋ …␋}␋␋output "offers" {␋ description \= "Offers" value \= { charm2\_metrics \= module.charm2.offers\["metrics"\] }␋}␋ | +| :---- | + +## Product modules + +Product modules are fully working solutions that can be easily deployed and customized using their input variables. Product modules are made up of charm modules, and/or component modules that are integrated one another to provide a set of end user functionalities. Integrator charms (grafana-agent, data-integrator, etc) are generally integrated in the solution at this stage, in order to facilitate the integration between the various components. Product modules are also expected to create the various resources needed by Charm modules and Component modules: + +* Juju models are defined in Product modules, and then fed to the various charm modules and component models that consume juju model UUIDs as inputs. Since product modules may generally be user-facing, product teams may decide to expose the model name as input variable to either create or consume a model. The TF resource (either juju\_model resource or data juju model) is then used to pass the related UUID to charm and component modules. + +```xml +data "juju_model" "this" { + owner = "admin" + name = var.this_name +} + +resource "juju_model" "that" { + name = var.that_namel +} + +module "charm_or_component" { + … + model_uuid = data.juju_model.this.uuid + … +} +``` + + + +* When charms/components require secrets to be created and fed into the charms as config options, their creations will be handled at product level +* When product modules create secrets from input variables, declare each credential variable with `sensitive = true` and `ephemeral = true`. Pair it with a boolean that controls creation and a number that triggers rotation. Both must be separate variables. `count` cannot read an ephemeral value, and Terraform cannot diff a write-only argument. On `juju_secret`, use the write-only `value_wo` and `value_wo_version` instead of `value`. A standard attribute writes its value in plain text to `terraform.tfstate` and to any plan saved with `-out`. With `value_wo`, Terraform sends the credential to the Juju controller only on create and on a version change, and writes `null` to state. Terraform or OpenTofu \>= 1.11 and provider `juju/juju` \>= 2.2.1 are required. Terraform needs the credential at apply as well as at plan. Supply it through `TF_VAR_` or `-var`. A `*.tfvars` file also works but leaves a plain-text copy on disk. DA289 has the full pattern, including map keys, rotation, grants, and migration. +* + +Product modules should always provide opinionated default options for dependent mandatory external components Mandatory components are defined as the ones required to provide minimal functionality. External non-mandatory components (e.g. COS, TLS when it is optional), may not be provided as part of product modules, but variables should be exposed to allow their integration. + +Product modules should be ready to be used and regularly tested to make sure that the underlying products properly integrate one another, and their integration is stable. Product modules may also be integrated and consumed by higher-level modules, although this is not required or mandatory. + +The charm modules and the component modules that are referenced in product modules must be pinned either using tags or commit hashes. **Floating references (e.g. branches) are not allowed.** To ensure reproducibility and deployment on air-gapped environments, revisions and images for every single charms bundled in the Product module must be exposed as input variables. Teams can decide whether defaults are set to null (therefore always fetching the latest revision on a given track) or to provide specific revisions (therefore ensuring that a given product module \- if not otherwise specified \- would always deploy the same revisions). Teams pinning the revisions should ensure that the default provided in the module have been tested by SolQA. Teams not pinning revisions should ensure a robust promotion process that revisions that are released to stable have been tested by SolQA. + +There can exist multiple product modules representing different deployment configurations of the product. Teams can decide whether to provide a single (or fewer) product module configurable or multiple static product modules (with more limited number of input variables) representing the various configurations. The trade off between the two options is up to the various teams, balancing the complexity of the terraform modules versus the need to maintain a large number of different modules. + +Product modules may or may not be cloud agnostic, depending on the scope of the product and the functionalities being used. If they are NOT cloud agnostic, different terraform modules should be provided for each supported cloud. + +### **Input** {#input} + +For Product modules there are limited mandatory requirements for input variables. +Inputs may vary depending on the level of customization to be allowed for a particular product. It is generally advised to expose an input variable of the same kind as the one defined in Charm modules and Component modules to allow customization of charm parameters. However, this is not strictly required, and charm authors can also decide to only expose a few high level configuration options, if the product is very opinionated on how to be deployed and configured. + +However, at the bare minimum, Product modules should: + +* Allow specification of the charm revision for every single charm. Allow specification of the images to be used for every single charm.For mandatory external integrations (e.g. Databases used by the applications, TLS when encryption is required), the product module should provide one specific implementation (e.g. self-signed certificates) while still allowing to integrate with a user-defined endpoint/offer. This can be achieved by bundling the “default” implementation with the \`count\` argument, that is set to 0 when the custom external integration is provided. +* For optional external integrations (e.g. COS), the product modules should expose the variables to allow the external integrations with user-defined endpoint/offers +* Environment specifications: + * Controller informations: controller endpoint, username, password and certificates to connect to the Juju controller + * Proxy + * Logging configurations + +#### Mandatory + +| Name | Type | Description | +| :---- | :---- | :---- | +| proxy | object | Provide proxy information `type = { http = … https = … no-proxy = … }` Mandatory only when the product module may create/manage its own juju model resources | +| logging-config | string | Logging configuration to be used. Mandatory only when the product module may create/manage its own juju model resources | +| risk | string | Flag to provide a risk level for solution. It generally controls the risk of the various components/charms included in the solution. The flag can be easily used by SolutionQA to test/gate different risk levels for the solution. | + +#### Recommendations + +Although there is no strict requirement, we still suggest some guidelines on how to organize the product specific input variables. In general, a Product module may provide inputs for: + +1. Configuring charm revisions and images by consuming store informations +2. Configuring specific charms, such as number of units of a particular instance of a database, specific configuration property of a product, etc +3. Customize the deployment structure, such which components of the solution to deploy, specification on which juju model to deploy different services, whether to share a database, etc +4. Networking specification, if multiple networks are to be used + +##### **Consuming charm revisions and images from store API** + +Product modules must provide configuration options to pin charm images and charm resources (OCI images). However, based on the default values of variables, three possible choices are possible: + +1. No defaults are provided, thus consuming latest versions from the store at deployment time +2. No defaults are provided, and at apply time, store information is resolved via [juju\_charm resource](https://registry.terraform.io/providers/juju/juju/latest/docs/data-sources/charm) and fed to charm/component modules. [See PoC implementation here](https://github.com/canonical/spark-k8s-bundle/pull/240). +3. Defaults are pinned to revisions/resources that have been tested by product teams + +Note that while 1 does not change over subsequent apply, 2 allows to always consume newer revisions from the store, but may introduce upgrades when applying/refreshing deployments. + +##### **Configuring specific charms** + +In order to configure specific charms we generally recommend to expose the input variables of the single Charm modules or Component modules, such that there is a clear structure and consistency with the different module inputs. Not all the input variables of the referenced submodules need to be exposed. When the module is a Charm module, the variable can have a flat structure, when the module is a Component module, the variable will provide a nested structure. + +| variable "component1" {␋ type \= object({␋ charm1 \= { app\_name \= … channel \= … … }␋ })␋}␋␋variable "charm2" {␋ type \= object({␋ app\_name \= … channel \= … …␋ })␋}␋ module "component1" {␋ source="..." charm1 \= var.component1.charm1␋} ␋ module "charm2" {␋ source="..."␋␋ name \= var.charm2.app\_name␋} | +| :---- | + +##### **Customize the deployment structure** + +Product modules can further customize the deployment using additional flags/variables. Such flags/variables are very custom and up to the product team to define. These customization may be at root level (e.g. risk level to be used across the solution, services to be included) or also at the component level (e.g. whether to use a new model and its name, toggle deployment of certain components). When they are at component level, their definition may be nested and bundled with the module inputs. + +| variable "component1" {␋ type \= object({ enable \= bool␋ model\_name \= string charm1 \= …␋ })␋} | +| :---- | + +##### **Networking configurations** + +The various networks to be used can be provided as a map(string) input, providing a mapping between specific keys and their respective network name, e.g. + +| variable "network\_bindings" {␋ type \= object({␋ \# Access to services from outside the deployment␋ external\_client\_access \= string␋ \# Access to services from inside the deployment␋ internal\_client\_access \= string␋ \# Network for replicating data between services␋ replication \= string␋ \# Network for managing the services␋ management \= string␋ \# Network for storage access␋ storage \= string␋ \# Network for backup of the service␋ backup \= string␋ })␋} | +| :---- | + +### **Output** {#output} + +#### Mandatory + +| Name | Type | Description | +| :---- | :---- | :---- | +| models | map(object) | Map of the key of the model and the components deployed in the model, e.g. `type = { model_uuid = … components = { charm1 = … charm2 = … } }` | +| metadata | object | Metadata of the product deployment, such as: `type = { version = … deployed_at = … updated_at = … }` An example of this implementation can be found [here](https://github.com/canonical/spark-k8s-bundle/blob/d0f8740e071b00fb15848f2f952021022943f461/terraform/products/charmed-spark-3.4/outputs.tf#L14). | + +#### Optional + +| Name | Type | Description | +| :---- | :---- | :---- | +| offersoffers | map(object)map(string) | Map of all offers exposed by the single charm. The key of the map should be compatible with the one provided in endpointsMap of the offers with their url that are being exposed by the model | +| credentials | map(object) | Set of credentials provided as output, e.g. `postgresql = { url = … username = … password = … ca = … } grafana = { url = … username = … password = … ca = … }` | + +##### **Example** + +`output "models" {` + `value = {` + `spark = {` + `model_uuid = juju_model.spark.uuid` + `components = merge(` + `module.spark.components, # component module` + `{zookeeper=module.zookeeper.application} # charm module` + `)` + `}` + `postgresql = {` + `model_uuid = juju_model.spark.uuid` + `components = {` + `postgresql=module.postgresql.application` + `}` + `}` +`}` + +### **Release cycle and versioning** {#release-cycle-and-versioning} + +Although product modules are not expected to allow (yet) for upgrades between versions, product modules will also be versioned using major.minor.patch convention, with similar guidelines as for Charm and Component modules for when using bumps across major, minor and patch versions. + +Product modules may reference other product modules. If the two modules live in the same repository (see “Module Lifecycle section”), relative reference can be used, e.g. + +| module "dependent\_product\_module" {␋ source="../\"␋} | +| :---- | + +If the referenced module lives in a different repository/modules, tags should be used when referencing repositories or versions when using product modules from the Terraform registry + +| module "dependent\_product\_module" {␋ source="...?ref=\"␋} | +| :---- | + +## Deployments {#deployments} + +Deployments represent a specific deployment instance of single Product modules or also combine Charm, Component and Product modules to address a given use-case or customer need. + +Deployments are defined and created during the implementation phase by either Field or Managed Solution, and they are then handed over and generally owned by the teams managing the deployments, e.g. Managed Solution, support or customers. SolQA tests may also be seen as a particular instance of a deployment, that however is not handed over, but rather torn down after the test is completed. However, as such, they need to version the various artifacts that can make the deployment reproducible. + +Terraform modules in the Deployment module must always be referenced by using tags to ensure reproducibility, e.g. + +* When consuming from a github repository + +| module "product\_module\_1" {␋ source="...?ref=\"␋} | +| :---- | + +* When consuming from a terraform registry + +| module "product\_module\_1" {␋ source="/path/to/terraform/module" version="X.Y.Z"␋} | +| :---- | + + **Floating references (e.g. branches) are not allowed.** + +Deployments are characterized by: + +1. Infrastructure definition, with bash scripts / Terraform modules / Guidance for setting up infrastructure +2. Pre-deployment and Post-deployment business logic and scripts +3. Product modules to be deployed +4. Input files (e.g. \`tfvars.json\` file) should be versioned to ensure reproducibility. In particular, for fully reproducible environments, it is mandatory to pin revisions/resource versions for all charms, to be fed to the product modules +5. Terraform state after deployment + +**Deployments should not be customizable in order to provide full reproducibility only considering the code**. Therefore, deployments do not have any inputs or outputs to be consumed. Even the same solution deployed to different environments (e.g. development, staging, pre-prod and production) should correspond to different deployments. + +The artifacts above must be versioned and committed to a specific repository, into a specific branch, such that future modifications of the plans can be version controlled and properly tracked. + +### **Input** + +#### Optional + +| Name | Type | Description | +| :---- | :---- | :---- | +| juju\_controllers | map(object) | Map associating a controller alias to controller information, such as endpoint, username, password `type = { endpoint = … username = … password = … ca = … }` Aliases can be referenced when establishing cross-controller relations | +| proxy | object | Provide proxy information `type = { http = … https = … no-proxy = … }` | +| logging\_config | string | Logging configuration to be used | + +##### **Managing controllers** + +Starting from Juju Terraform provider 1.3.0+, a \`juju\_controller\` resource has been added to manage Juju controllers within the Terraform module. See more information about the resource in the [Juju Terraform provider documentation.](https://registry.terraform.io/providers/juju/juju/2.1.0/docs/resources/controller) + +The definition of \`juju\_controller\` resources should be done at the level of the “Deployment” module, to make product modules agnostic with respect to the controller where they are deployed. Product modules can however consume external controllers to enable cross-controller relations. The information of the various controllers must be provided as input variables of the Deployment module using the \`juju\_controllers\` input described above, providing a map of aliases, e.g. + +juju\_controllers \= { + cos \= { + endpoint \= … + username \= … + password \= … + ca \= … + } +} + +The alias can then be used when creating cross-controller relations, e.g. +module "spark" { + depends\_on \= \[juju\_model.spark, module.cos\] + … + + cos \= { + dashboard \= { + Kind \= “offer” + controller \= “cos” + url \= var.dashboard\_url + } + … +} + +# Further Information + +## Projects + +The following table provides a list of the projects known to be compliant with the current spec + +| Project | Repository/Branch | +| :---- | :---- | +| Kubeflow | https://github.com/canonical/charmed-kubeflow-solutions/tree/feat/terraform-refactor | +| Spark | https://github.com/canonical/spark-k8s-bundle | + +## PoC + +A proof of concept implementation of the standard has been implemented in the following terraform module: + +[https://github.com/canonical/charmed-kubeflow-solutions/tree/wip-tf-refactoring/modules/refactoring](https://github.com/canonical/charmed-kubeflow-solutions/tree/wip-tf-refactoring/modules/refactoring) + +A few remarks: + +* The top level TF module represent an example of a Product Module +* The modules implemented in the \`modules\` folder represents example of Component Modules +* A few Charm modules have been used in the Product modules when referring to databases (e.g mysql-k8s) +* There is no Deployment module yet + +## Prior Art + +The current specifications has been consolidated starting from the various standards/attempts that have been made over the years: + +* [CC006 - Terraform modules organization in the Juju ecosystem](https://docs.google.com/document/d/1EG71A2pJ244PQRaGVzGj7Mx2B_bzE4U_OSqx4eeVI1E/edit?tab=t.0), +* [SQ065 - Terraform Module On-boarding Requirements for Testing](https://docs.google.com/document/d/1JaJp-RTOxP1zqCMZqgQdSXI8coBuiqhFqmt5OPWjNNM/edit?tab=t.0) +* [DA219 - Data Platform Terraform Modules Structure](https://docs.google.com/document/d/1CWMzbXUxVfMHxGthWNmm8VNsBZKjyi2jRoBS4QWbkIo/edit?tab=t.0) +* Terraform Mini-sprint in London + * [Github repo](https://github.com/canonical/terraform-minisprint-terragrunt/tree/main) + * [Solution Deployments With Terraform & Terragrunt](https://docs.google.com/presentation/d/1mFQzi2asJAIrfj7zkXhNUvUa88bbiGt7nCN8JCu6MHY/edit?slide=id.g344d9fe6fa5_0_43#slide=id.g344d9fe6fa5_0_43)) + +Various Terraform modules have been evaluated when writing this specification: + +* [Kubeflow](https://github.com/canonical/charmed-kubeflow-solutions) +* [Spark](https://github.com/canonical/spark-k8s-bundle/tree/main/releases/3.5/terraform) +* [Kafka Bundle](https://github.com/canonical/kafka-bundle/tree/main/terraform) +* [OpenSearch](https://github.com/canonical/opensearch-operator/tree/2/edge/terraform) +* [Observability Stack](https://github.com/canonical/observability-stack) +* [Telco SD Core](https://github.com/canonical/terraform-juju-sdcore) + +# Spec History and Changelog + +Please be thorough when recording changes and progress with the spec itself and the work resulting from it. Record every meeting, attendees and conclusions from the meeting. + +| Author(s) | Status | Date | Comment | +| :---- | :---- | :---- | :---- | +| [Enrico Deusebio](mailto:enrico.deusebio@canonical.com) | Drafting | Nov 26, 2025 | Initial Draft | +| [Enrico Deusebio](mailto:enrico.deusebio@canonical.com) | Pending Review | Jan 7, 2026 | First round of review | +| [Enrico Deusebio](mailto:enrico.deusebio@canonical.com) | Pending Review | Feb 12, 2026 | Second round of review | +| [Jon Seager](mailto:jon.seager@canonical.com) | Approved | Feb 12, 2026 | Jon’s approval | +| [Enrico Deusebio](mailto:enrico.deusebio@canonical.com) | Drafting | May 12, 2026 | 26.04 Improvements | +| [Enrico Deusebio](mailto:enrico.deusebio@canonical.com) | Pending Review | Jun 29, 2026 | 26.04-post review | +| | | | | + diff --git a/.github/skills/terraform-cc008-migration/scripts/check_cc008.sh b/.github/skills/terraform-cc008-migration/scripts/check_cc008.sh new file mode 100755 index 0000000000..ac97d401a8 --- /dev/null +++ b/.github/skills/terraform-cc008-migration/scripts/check_cc008.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +# Run the CC008 Terraform compliance checker from canonical/operator-workflows +# against the given module directories. With no arguments, every directory +# containing a main.tf is discovered and checked. + +set -euo pipefail + +CHECKER_BASE_URL="https://raw.githubusercontent.com/canonical/operator-workflows/main/terraform-compliance" +HCL2_VERSION="8.1.3" + +if ! command -v uv &> /dev/null; then + echo "❌ Error: uv is not installed. See https://docs.astral.sh/uv/." >&2 + exit 1 +fi + +module_dirs=("$@") + +if [ ${#module_dirs[@]} -eq 0 ]; then + while IFS= read -r module_dir; do + module_dirs+=("$module_dir") + done < <(find . -type f -name 'main.tf' -not -path '*/.terraform/*' -exec dirname {} \; | sort -u) +fi + +if [ ${#module_dirs[@]} -eq 0 ]; then + echo "❌ Error: no Terraform module directory found (no main.tf)." >&2 + exit 1 +fi + +scratch_dir="$(mktemp -d)" +trap 'rm -rf "$scratch_dir"' EXIT + +echo "⬇️ Fetching the CC008 compliance checker..." +for checker_file in terraform_spec.py terraform_hcl.py terraform_check.py; do + curl -fsSL -o "$scratch_dir/$checker_file" "$CHECKER_BASE_URL/$checker_file" +done + +echo "🔍 Checking CC008 compliance for: ${module_dirs[*]}" +uv run --with "python-hcl2==$HCL2_VERSION" python "$scratch_dir/terraform_check.py" --verbose "${module_dirs[@]}"