Skip to content

fix(terraform): allow juju provider 1.x and 2.x#1657

Draft
marceloneppel wants to merge 14 commits into
16/edgefrom
feature/fix-juju-terraform-provider-version-constraint
Draft

fix(terraform): allow juju provider 1.x and 2.x#1657
marceloneppel wants to merge 14 commits into
16/edgefrom
feature/fix-juju-terraform-provider-version-constraint

Conversation

@marceloneppel

@marceloneppel marceloneppel commented Jul 14, 2026

Copy link
Copy Markdown
Member

Issue

The terraform module's juju provider constraint was pinned to ~> 1.0 (>= 1.0.0, < 2.0.0). A downstream consumer reported pairing this module with a sibling module requiring juju provider >= 1.1.x and hitting a constraint conflict.

Reproducing on a Linux box showed ~> 1.0 does not conflict with >= 1.1.1 — both resolve to juju provider v1.5.6 (the ~> 1.0 operator means >= 1.0.0, < 2.0.0, not < 1.1.0; the conflicting ~> 1.0.0 form only ever existed on unmerged branches and never shipped in a tag). The real conflict is the v2 cap: a higher-level module requiring juju provider 2.x (v2.0.0 released May 2026, v2.1.1 current; supports Juju 3.x and 4.x, drops 2.9) hits the < 2.0.0 upper bound and fails terraform init. Per the upstream v2 release notes the v1→v2 upgrade is non-breaking for the resource surface this module uses (a single juju_application).

Separately, the module shipped with no terraform CIterraform/** was in ci.yaml's paths-ignore, so terraform-only changes ran no checks at all and a broken/unresolvable constraint would land unnoticed.

Solution

Widen the constraintterraform/versions.tf: ~> 1.0>= 1.0, < 3.0. Allows both v1.x and v2.x consumers while keeping a ceiling against an unvetted major 3.x. v1 consumers still resolve to the latest 1.x (e.g. v1.5.6); v2 consumers resolve to the latest 2.x (e.g. v2.1.1); beyond-cap constraints fail as expected. The generated terraform/README.md tables are regenerated to match.

Add a terraform test lane (mirrors canonical/iam-bundle-integration's static checks and canonical/mysql-plans' deploy test):

  • .github/workflows/terraform.yaml — triggered on terraform/** changes (the inverse of the path-ignore). Installs terraform via snap, then runs terraform fmt/init/validate and the constraint-composition suite.
  • tests/terraform/test_compositions.py + tox -e terraform — 5 parametrized cases sourcing the real module alongside a sibling juju provider constraint (v1, identity >= 1.1.1, v2, v2-pinned, beyond-cap) and asserting terraform init resolves to the expected provider major or fails for an unsatisfiable-constraint reason. Regression guard for the widened constraint.
  • tests/integration/terraform/test_terraform_deploy.py + tests/spread/test_terraform_deploy.py/task.yaml — jubilant test applying the module (published 16/stable charm) into the concierge testing model and waiting for active/idle, failing fast on an app error. Runs via the existing spread integration lane (microk8s); no actions-operator.
  • TF_BINARY env override in both test lanes (and the relevant tox envs) to point CI at a specific terraform build.
  • terraform/.gitignore — ignore .terraform/ and the lock file, standard hygiene for a module consumed as a git source.

Checklist

  • I have added or updated any relevant documentation.
  • I have cleaned any remaining cloud resources from my accounts.

The juju terraform provider constraint was pinned to '~> 1.0'
(>= 1.0.0, < 2.0.0), which blocks higher-level modules that require
juju provider 2.x (Juju 4.x controllers). Widening to '>= 1.0, < 3.0'
lets the module compose with both v1 and v2 consumers while keeping a
ceiling against an unvetted major 3.x.

v1 consumers still resolve to the latest 1.x (e.g. v1.5.6); v2 consumers
resolve to the latest 2.x (e.g. v2.1.1). Unsatisfiable and beyond-cap
constraints fail as expected. The v1->v2 provider upgrade is non-breaking
for the resource surface this module uses (juju_application).

Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com>
The terraform module shipped with no CI: 'terraform/**' was in ci.yaml's
paths-ignore, so terraform-only changes ran no checks and a broken/unresolvable
provider constraint would land unnoticed (the exact failure mode downstream
consumers hit when pairing this module with a juju provider 2.x requirement).

Add a lightweight lane mirroring canonical/iam-bundle-integration's static
checks, plus a real-deploy test like canonical/charmed-kubeflow-solutions:

- .github/workflows/terraform.yaml: triggered on terraform/** changes (the
  inverse of the path-ignore). Static job runs fmt/init/validate against the
  live registry, then the constraint-composition suite.
- tests/terraform/test_compositions.py (tox -e terraform): parametrized cases
  that source the real module alongside a sibling juju provider constraint
  (v1, v2, unsatisfiable, beyond-cap) and assert terraform init resolves (or
  fails) as expected. This is the regression guard for the constraint widened in
  the previous commit.
- tests/integration/terraform/test_terraform_deploy.py: jubilant test that
  applies the module (published 16/stable charm) into the concierge 'testing'
  model and waits for active/idle. Runs via the existing spread integration
  lane (1:1 task at tests/spread/test_terraform_deploy.py); no actions-operator.
- terraform/.gitignore: ignore .terraform/ and the lock file so a consumed
  module never ships a pin that overrides downstream constraint resolution.

Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com>
@marceloneppel marceloneppel changed the base branch from main to 16/edge July 14, 2026 16:13
@marceloneppel marceloneppel added the bug Something isn't working as expected label Jul 14, 2026
@github-actions github-actions Bot added the Libraries: Out of sync The charm libs used are out-of-sync label Jul 14, 2026
…action

The repo's zizmor policy (.github/zizmor.yaml) allowlists third-party
actions to a trusted set (canonical/*, actions/*, docker/login-action,
tiobe/tics-github-action); hashicorp/setup-terraform is not on it, so the
Lint .github/workflows/ job failed with forbidden-uses (and unpinned-uses).

Installing terraform via snap needs no allowlist entry and adds no
RCE-capable action to the trusted set, matching how the spread deploy task
already provisions terraform. Verified with zizmor 1.27.0: no findings.

Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com>
The generated terraform-docs tables still showed the juju provider as
'~> 1.0.0' after the constraint was widened to '>= 1.0, < 3.0'. Sync the
Requirements and Providers tables so the rendered module docs match
versions.tf.

Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com>
…e reason

The '>= 2.9' case only failed because juju provider 2.9 does not exist yet;
once a 2.9.x ships (inside the '< 3.0' cap) it would resolve successfully and
flip the case to spuriously red, so it guarded nothing durable. Drop it and
keep '>= 3.0' (beyond_cap), which stays correct after a 3.x releases because
the cap keeps blocking it. Tighten the negative branch to assert the init
failure is an unsatisfiable-constraint error rather than any non-zero exit,
so an unrelated failure can no longer pass the case.

Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com>
Without an error predicate, juju.wait() spins the full 20-minute timeout if
postgresql-k8s lands in an error state before raising. Pass
error=jubilant.any_error so the test fails immediately with a legible cause,
matching the repo's other jubilant integration tests.

Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com>
CI or a developer may need to point the tests at a specific terraform
build rather than whatever is first on PATH. Honor a TF_BINARY env var
(falling back to "terraform") in both the composition and deploy tests,
and pass it through the relevant tox envs. Matches the convention used by
canonical/mysql-plans.

Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com>
…eploy test

On CI the arm64 leg hung for ~2h inside 'terraform init' (a transient
registry/network stall) and only stopped at the spread kill-timeout, because
the subprocess had no timeout and captured its output — so the stall was
neither bounded nor visible in the log.

Pass timeout=10m and let terraform stream to the job log (drop capture_output),
using check=True to surface a non-zero exit. A stall now fails the test in
minutes with live output instead of wedging the runner. Matches the pattern in
canonical/mysql-plans' deploy test.

Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com>
The previous fix used a single 10m timeout for both terraform commands. On the
arm64 CI substrate 'terraform apply' — which the juju provider blocks on while
it deploys postgresql-k8s and waits for it to settle — exceeded 10m mid-deploy
(log showed 'Still creating...' progressing), so the test failed on a legit but
slow deploy rather than a real fault.

Split the budgets: 'init' keeps a tight 5m cap (it only downloads the provider,
and guards the transient registry stall seen earlier), while 'apply' gets the
same 20m budget as the deploy wait that follows it. amd64 already passes well
under this; the wider apply budget covers the slower arm64 deploy.

Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com>
… default

The module defaults constraints to 'arch=amd64'. On the arm64 CI runner the juju
provider turned that into a pod nodeSelector 'kubernetes.io/arch=amd64', which no
arm64 node satisfies, so postgresql-k8s stayed Pending (FailedScheduling) and
'terraform apply' blocked until the timeout — misdiagnosed earlier as a slow
deploy needing a bigger timeout.

Pass 'constraints=arch=$(dpkg --print-architecture)' so the module deploys for
the host's architecture, mirroring what spread's prepare-each already does at the
model level for every other integration test. Verified on an arm64 box: the pod
schedules immediately and postgresql-k8s reaches active (apply returns in ~40s
once units are created; juju.wait then covers settle). Collapse the two terraform
timeouts into one 15m bound now that apply is not the long pole.

Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com>
Three gaps surfaced by an adversarial comparison to canonical/mysql-plans:

1. fmt lacked -diff: a failing fmt -check exited non-zero with no diff, so a
   contributor couldn't see what to fix. Add -diff (and a locally-runnable
   [testenv:lint-terraform] tox env mirroring fmt/init/validate), so the same
   lint runs locally via 'tox run -e lint-terraform' as in CI. The workflow
   now calls that env instead of inlining three commands.

2. Deploy test only passed juju_model + constraints, leaving storage_directives
   and config unexercised — a postgres-critical wiring hole. Pass storage
   directives (data/archive/logs/temp) and a profile config, and assert the
   application_name module output after apply.

Validated: tox run -e lint-terraform passes (fmt -diff, init, validate);
the -var HCL map syntax parses (terraform validate with vars); composition
tests 5/5.

Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com>
The deploy test's `_run_terraform` helper runs `terraform` without
capture_output so the slow init/apply stream their progress live to the log
(a stall stays visible). The final `terraform output -raw application_name`
call reused that helper, so its `CompletedProcess.stdout` was None and the
assertion `output.stdout.strip()` raised AttributeError on both the amd64 and
arm64 legs, even though the deploy itself reached active/idle.

Give the helper an opt-in `capture` and set it only on the output call, so
init/apply keep streaming while the assertion reads a real captured value.

Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com>
The Usage examples invoked `-var='juju_model_name=...'` and
`-var='storage={...}'`, but the module declares neither variable: the model
input is `juju_model` (a model UUID, not a name) and the storage input is
`storage_directives`. Every documented `terraform apply` therefore failed with
an undeclared-variable error.

Point the examples at the real variables, show capturing the model UUID (as
the deploy test does), and drop the LXD-only storage pool specifiers
(`,lxd`, `tmpfs,`) that do not apply to this Kubernetes module.

Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working as expected Libraries: Out of sync The charm libs used are out-of-sync

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant