From a4f76e46b29f7dd7d8f06d8d7ff41d5abff24bc9 Mon Sep 17 00:00:00 2001 From: "Jan-Yaeger Dhillon (charmkeeper)" Date: Fri, 24 Apr 2026 15:49:36 -0600 Subject: [PATCH 01/11] chore(terraform): update terraform modules to latest standards - Update required_version to ~> 1.12 and juju provider to ~> 1.0 - Add terraform/.tflint.hcl at the terraform root with required_version rule - Add renovate.json with charmhub custom datasource for automated revision tracking - Add tftest setup modules for charm and product modules - Fix copyright year typo in outputs.tf (2026 -> 2025) - Update README docs sections to reflect new version constraints Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- renovate.json | 79 +++++++++++++++++++ terraform/.tflint.hcl | 5 ++ terraform/charm/README.md | 2 +- terraform/charm/outputs.tf | 2 +- terraform/charm/tests/main.tftest.hcl | 21 +++++ terraform/charm/tests/setup/main.tf | 21 +++++ terraform/charm/versions.tf | 2 +- .../modules/landscape-scalable/README.md | 2 +- .../landscape-scalable/tests/main.tftest.hcl | 23 ++++++ .../landscape-scalable/tests/setup/main.tf | 21 +++++ .../modules/landscape-scalable/versions.tf | 2 +- 11 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 renovate.json create mode 100644 terraform/.tflint.hcl create mode 100644 terraform/charm/tests/main.tftest.hcl create mode 100644 terraform/charm/tests/setup/main.tf create mode 100644 terraform/product/modules/landscape-scalable/tests/main.tftest.hcl create mode 100644 terraform/product/modules/landscape-scalable/tests/setup/main.tf diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..d6ede418 --- /dev/null +++ b/renovate.json @@ -0,0 +1,79 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "automerge": true, + "customDatasources": { + "charmhub": { + "defaultRegistryUrlTemplate": "https://api.charmhub.io/v2/charms/info/{{packageName}}?fields=channel-map", + "format": "json", + "transformTemplates": [ + "{\"releases\": [{\"version\": $string($$.(`channel-map`[channel.risk = 'edge' and channel.track = 'latest' and channel.base.architecture = 'amd64' and channel.base.channel = '24.04'].revision.revision))}]}" + ] + } + }, + "customManagers": [ + { + "customType": "regex", + "datasourceTemplate": "docker", + "description": "Update base image references", + "managerFilePatterns": [ + "/(^|/)rockcraft.yaml$/" + ], + "matchStrings": [ + "# renovate: build-base:\\s+(?[^:]*):(?[^\\s@]*)(@(?sha256:[0-9a-f]*))?", + "# renovate: base:\\s+(?[^:]*):(?[^\\s@]*)(@(?sha256:[0-9a-f]*))?" + ], + "matchStringsStrategy": "any", + "versioningTemplate": "ubuntu" + }, + { + "customType": "regex", + "datasourceTemplate": "custom.charmhub", + "fileMatch": [ + "\\.tftest\\.hcl$", + "\\.tf$" + ], + "matchStrings": [ + "# renovate: depName=\"(?[^\"]+)\"\\s*\\n\\s*(?[a-zA-Z0-9_]+)\\s*=\\s*(?\\d+)" + ], + "versioningTemplate": "semver-coerced" + } + ], + "extends": [ + "config:recommended", + "group:allNonMajor" + ], + "ignorePaths": [], + "packageRules": [ + { + "enabled": true, + "matchDatasources": [ + "docker" + ], + "pinDigests": true + }, + { + "automerge": true, + "enabled": true, + "matchDatasources": [ + "custom.charmhub" + ] + }, + { + "enabled": false, + "matchFileNames": [ + "rockcraft.yaml" + ], + "matchUpdateTypes": [ + "major", + "minor", + "patch" + ] + } + ], + "schedule": [ + "* * * * 0,6" + ], + "vulnerabilityAlerts": { + "enabled": true + } +} diff --git a/terraform/.tflint.hcl b/terraform/.tflint.hcl new file mode 100644 index 00000000..ee21740d --- /dev/null +++ b/terraform/.tflint.hcl @@ -0,0 +1,5 @@ +# © 2025 Canonical Ltd. + +rule "terraform_required_version" { + enabled = true +} diff --git a/terraform/charm/README.md b/terraform/charm/README.md index 2f34b2ce..624e9cb0 100644 --- a/terraform/charm/README.md +++ b/terraform/charm/README.md @@ -91,7 +91,7 @@ make fix-charm-module | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.10 | +| [terraform](#requirement\_terraform) | ~> 1.12 | | [juju](#requirement\_juju) | ~> 1.0 | ## Providers diff --git a/terraform/charm/outputs.tf b/terraform/charm/outputs.tf index 922065c6..a38173ea 100644 --- a/terraform/charm/outputs.tf +++ b/terraform/charm/outputs.tf @@ -1,4 +1,4 @@ -# © 2026 Canonical Ltd. +# © 2025 Canonical Ltd. # The following outputs are meant to conform with Canonical's standards for # charm modules in a Terraform ecosystem (CC008). diff --git a/terraform/charm/tests/main.tftest.hcl b/terraform/charm/tests/main.tftest.hcl new file mode 100644 index 00000000..0bdffb1c --- /dev/null +++ b/terraform/charm/tests/main.tftest.hcl @@ -0,0 +1,21 @@ +# © 2025 Canonical Ltd. + +run "setup_tests" { + module { + source = "./tests/setup" + } +} + +run "basic_deploy" { + variables { + model_uuid = run.setup_tests.model_uuid + channel = "latest/edge" + # renovate: depName="landscape-server" + revision = 143 + } + + assert { + condition = output.app_name == "landscape-server" + error_message = "landscape-server app_name did not match expected" + } +} diff --git a/terraform/charm/tests/setup/main.tf b/terraform/charm/tests/setup/main.tf new file mode 100644 index 00000000..56abf817 --- /dev/null +++ b/terraform/charm/tests/setup/main.tf @@ -0,0 +1,21 @@ +# © 2025 Canonical Ltd. + +terraform { + required_version = "~> 1.12" + required_providers { + juju = { + version = "~> 1.0" + source = "juju/juju" + } + } +} + +provider "juju" {} + +resource "juju_model" "test_model" { + name = "tf-testing-${formatdate("YYYYMMDDhhmmss", timestamp())}" +} + +output "model_uuid" { + value = juju_model.test_model.uuid +} diff --git a/terraform/charm/versions.tf b/terraform/charm/versions.tf index 72e33ceb..fbf26ab1 100644 --- a/terraform/charm/versions.tf +++ b/terraform/charm/versions.tf @@ -1,7 +1,7 @@ # © 2025 Canonical Ltd. terraform { - required_version = ">= 1.10" + required_version = "~> 1.12" required_providers { juju = { source = "juju/juju" diff --git a/terraform/product/modules/landscape-scalable/README.md b/terraform/product/modules/landscape-scalable/README.md index 33a8a6c0..a8eb7748 100644 --- a/terraform/product/modules/landscape-scalable/README.md +++ b/terraform/product/modules/landscape-scalable/README.md @@ -58,7 +58,7 @@ This module uses the [Landscape Server charm module](https://github.com/canonica | Name | Version | | ------------------------------------------------------------------------- | ------- | -| [terraform](#requirement\_terraform) | >= 1.10 | +| [terraform](#requirement\_terraform) | ~> 1.12 | | [juju](#requirement\_juju) | ~> 1.0 | ## Providers diff --git a/terraform/product/modules/landscape-scalable/tests/main.tftest.hcl b/terraform/product/modules/landscape-scalable/tests/main.tftest.hcl new file mode 100644 index 00000000..98758345 --- /dev/null +++ b/terraform/product/modules/landscape-scalable/tests/main.tftest.hcl @@ -0,0 +1,23 @@ +# © 2025 Canonical Ltd. + +run "setup_tests" { + module { + source = "./tests/setup" + } +} + +run "basic_deploy" { + variables { + model_uuid = run.setup_tests.model_uuid + landscape_server = { + channel = "latest/edge" + # renovate: depName="landscape-server" + revision = 143 + } + } + + assert { + condition = output.applications.landscape_server.app_name == "landscape-server" + error_message = "landscape-server app_name did not match expected" + } +} diff --git a/terraform/product/modules/landscape-scalable/tests/setup/main.tf b/terraform/product/modules/landscape-scalable/tests/setup/main.tf new file mode 100644 index 00000000..56abf817 --- /dev/null +++ b/terraform/product/modules/landscape-scalable/tests/setup/main.tf @@ -0,0 +1,21 @@ +# © 2025 Canonical Ltd. + +terraform { + required_version = "~> 1.12" + required_providers { + juju = { + version = "~> 1.0" + source = "juju/juju" + } + } +} + +provider "juju" {} + +resource "juju_model" "test_model" { + name = "tf-testing-${formatdate("YYYYMMDDhhmmss", timestamp())}" +} + +output "model_uuid" { + value = juju_model.test_model.uuid +} diff --git a/terraform/product/modules/landscape-scalable/versions.tf b/terraform/product/modules/landscape-scalable/versions.tf index 72e33ceb..fbf26ab1 100644 --- a/terraform/product/modules/landscape-scalable/versions.tf +++ b/terraform/product/modules/landscape-scalable/versions.tf @@ -1,7 +1,7 @@ # © 2025 Canonical Ltd. terraform { - required_version = ">= 1.10" + required_version = "~> 1.12" required_providers { juju = { source = "juju/juju" From 22819f592beaa70820300793a5db9fc6b583d967 Mon Sep 17 00:00:00 2001 From: "Jan-Yaeger Dhillon (charmkeeper)" Date: Fri, 24 Apr 2026 16:39:17 -0600 Subject: [PATCH 02/11] fix(terraform): revert copyright year, add workflow, fix CI terraform version - Revert outputs.tf copyright year to 2026 (was wrongly changed to 2025) - Add missing .github/workflows/test_terraform_modules.yaml (requires workflow PAT scope) - Update CI to use terraform ~1.12 to match required_version in modules Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/lint.yaml | 2 +- .github/workflows/test_terraform_modules.yaml | 18 ++++++++++++++++++ .github/workflows/unit-test.yaml | 2 +- terraform/charm/outputs.tf | 2 +- 4 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/test_terraform_modules.yaml diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 5ea9d19b..676ccf40 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -31,7 +31,7 @@ jobs: - uses: actions/checkout@v5 - uses: hashicorp/setup-terraform@v3 with: - terraform_version: "~1.10" + terraform_version: "~1.12" - uses: terraform-linters/setup-tflint@v4 - run: make terraform-check-all diff --git a/.github/workflows/test_terraform_modules.yaml b/.github/workflows/test_terraform_modules.yaml new file mode 100644 index 00000000..b615fd2b --- /dev/null +++ b/.github/workflows/test_terraform_modules.yaml @@ -0,0 +1,18 @@ +# Copyright 2025 Canonical Ltd. +# See LICENSE file for licensing details. + +name: Terraform modules tests + +on: + workflow_dispatch: + pull_request: + paths: + - 'terraform/**' + +jobs: + terraform-tests: + uses: canonical/operator-workflows/.github/workflows/terraform_modules_test.yaml@main + secrets: inherit + with: + lxd-controller: true + terraform-directories: '["terraform/charm", "terraform/product/modules/landscape-scalable"]' diff --git a/.github/workflows/unit-test.yaml b/.github/workflows/unit-test.yaml index 8c1f64df..e7cb13e8 100644 --- a/.github/workflows/unit-test.yaml +++ b/.github/workflows/unit-test.yaml @@ -42,7 +42,7 @@ jobs: - uses: hashicorp/setup-terraform@v3 with: - terraform_version: "~1.10" + terraform_version: "~1.12" - name: Run terraform tests run: make terraform-test-all diff --git a/terraform/charm/outputs.tf b/terraform/charm/outputs.tf index a38173ea..922065c6 100644 --- a/terraform/charm/outputs.tf +++ b/terraform/charm/outputs.tf @@ -1,4 +1,4 @@ -# © 2025 Canonical Ltd. +# © 2026 Canonical Ltd. # The following outputs are meant to conform with Canonical's standards for # charm modules in a Terraform ecosystem (CC008). From 7bc0fb90ae33107f0a10537644e3a003a79da34f Mon Sep 17 00:00:00 2001 From: "Jan-Yaeger Dhillon (charmkeeper)" Date: Fri, 24 Apr 2026 16:43:38 -0600 Subject: [PATCH 03/11] fix(terraform): filter integration tests from unit test Makefile targets The main.tftest.hcl files require a real Juju controller and should only run in the integration test workflow (test_terraform_modules.yaml with lxd-controller: true). Update Makefile targets to filter to mock-only test files for the unit test CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- terraform/charm/Makefile | 2 +- terraform/product/Makefile | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/terraform/charm/Makefile b/terraform/charm/Makefile index b2cc8f4f..64bb1b5a 100644 --- a/terraform/charm/Makefile +++ b/terraform/charm/Makefile @@ -8,7 +8,7 @@ test-charm-module: terraform init -backend=false && \ - terraform test + terraform test -filter=tests/outputs.tftest.hcl fmt-check: terraform init -backend=false && \ diff --git a/terraform/product/Makefile b/terraform/product/Makefile index a3f30020..4ea9a681 100644 --- a/terraform/product/Makefile +++ b/terraform/product/Makefile @@ -24,5 +24,8 @@ test-product-modules: for m in $(MODULE_PATHS); do \ cd $$m && \ terraform init -backend=false && \ - terraform test; \ + terraform test \ + -filter=tests/defaults.tftest.hcl \ + -filter=tests/outputs.tftest.hcl \ + -filter=tests/integrations.tftest.hcl; \ done From 1dc5cfc15921cc2af88111a5ba8785f5a5815c66 Mon Sep 17 00:00:00 2001 From: jansdhillon Date: Fri, 5 Jun 2026 11:25:33 -0600 Subject: [PATCH 04/11] bump copyright to 2026 --- .github/workflows/build.yaml | 2 +- .github/workflows/lint.yaml | 2 +- .github/workflows/test_terraform_modules.yaml | 4 +- .github/workflows/unit-test.yaml | 2 +- actions.yaml | 2 +- copyright | 2 +- lib/charms/grafana_agent/LICENSE | 4 +- lib/charms/grafana_agent/v0/cos_agent.py | 2 +- lib/charms/operator_libs_linux/v0/apt.py | 2 +- lib/charms/operator_libs_linux/v0/passwd.py | 2 +- lib/charms/operator_libs_linux/v1/systemd.py | 2 +- lib/charms/smtp_integrator/v0/smtp.py | 2 +- metadata.yaml | 2 +- src/charm.py | 156 ++++++++--------- src/settings_files.py | 2 +- terraform/.tflint.hcl | 2 +- terraform/charm/main.tf | 2 +- terraform/charm/tests/main.tftest.hcl | 2 +- terraform/charm/tests/outputs.tftest.hcl | 2 +- terraform/charm/tests/setup/main.tf | 2 +- terraform/charm/variables.tf | 2 +- terraform/charm/versions.tf | 2 +- .../modules/landscape-scalable/outputs.tf | 2 +- .../tests/defaults.tftest.hcl | 2 +- .../tests/integrations.tftest.hcl | 2 +- .../landscape-scalable/tests/main.tftest.hcl | 2 +- .../tests/outputs.tftest.hcl | 2 +- .../landscape-scalable/tests/setup/main.tf | 2 +- .../modules/landscape-scalable/versions.tf | 2 +- tests/unit/test_charm.py | 158 ++++++++---------- tests/unit/test_settings_files.py | 2 +- 31 files changed, 167 insertions(+), 209 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 50a2014c..a6c72161 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,4 +1,4 @@ -# Copyright 2025 Canonical Ltd. +# Copyright 2026 Canonical Ltd. # See LICENSE file for licensing details. # Adapted from: https://github.com/canonical/postgresql-operator/blob/main/.github/workflows/ci.yaml name: Build charm diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 676ccf40..4fcac0e8 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -1,4 +1,4 @@ -# Copyright 2025 Canonical Ltd. +# Copyright 2026 Canonical Ltd. # See LICENSE file for licensing details. # Adapted from: https://github.com/canonical/postgresql-operator/blob/main/.github/workflows/ci.yaml name: Lint charm diff --git a/.github/workflows/test_terraform_modules.yaml b/.github/workflows/test_terraform_modules.yaml index b615fd2b..a2759ef2 100644 --- a/.github/workflows/test_terraform_modules.yaml +++ b/.github/workflows/test_terraform_modules.yaml @@ -1,4 +1,4 @@ -# Copyright 2025 Canonical Ltd. +# Copyright 2026 Canonical Ltd. # See LICENSE file for licensing details. name: Terraform modules tests @@ -7,7 +7,7 @@ on: workflow_dispatch: pull_request: paths: - - 'terraform/**' + - "terraform/**" jobs: terraform-tests: diff --git a/.github/workflows/unit-test.yaml b/.github/workflows/unit-test.yaml index e7cb13e8..28600be0 100644 --- a/.github/workflows/unit-test.yaml +++ b/.github/workflows/unit-test.yaml @@ -1,4 +1,4 @@ -# Copyright 2025 Canonical Ltd. +# Copyright 2026 Canonical Ltd. # See LICENSE file for licensing details. # Adapted from: https://github.com/canonical/postgresql-operator/blob/main/.github/workflows/ci.yaml name: Unit test charm diff --git a/actions.yaml b/actions.yaml index 94ef6b3c..8c0a4596 100644 --- a/actions.yaml +++ b/actions.yaml @@ -1,4 +1,4 @@ -# Copyright 2025 Canonical Ltd +# Copyright 2026 Canonical Ltd # See LICENSE file for licensing details. hash-id-databases: diff --git a/copyright b/copyright index a285045c..9bab65d1 100644 --- a/copyright +++ b/copyright @@ -1,7 +1,7 @@ Format: http://dep.debian.net/deps/dep5/ Files: * -Copyright: Copyright 2025, Canonical Ltd., All Rights Reserved. +Copyright: Copyright 2026, Canonical Ltd., All Rights Reserved. License: GPL-2 This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/lib/charms/grafana_agent/LICENSE b/lib/charms/grafana_agent/LICENSE index 8a83a559..4fe1d7e2 100644 --- a/lib/charms/grafana_agent/LICENSE +++ b/lib/charms/grafana_agent/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2025 Canonical Ltd. + Copyright 2026 Canonical Ltd. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -198,4 +198,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file + limitations under the License. diff --git a/lib/charms/grafana_agent/v0/cos_agent.py b/lib/charms/grafana_agent/v0/cos_agent.py index bbc894bf..366f4853 100644 --- a/lib/charms/grafana_agent/v0/cos_agent.py +++ b/lib/charms/grafana_agent/v0/cos_agent.py @@ -1,4 +1,4 @@ -# Copyright 2025 Canonical Ltd. +# Copyright 2026 Canonical Ltd. # See LICENSE file for licensing details. r"""## Overview. diff --git a/lib/charms/operator_libs_linux/v0/apt.py b/lib/charms/operator_libs_linux/v0/apt.py index cb09f6f4..17e71e02 100644 --- a/lib/charms/operator_libs_linux/v0/apt.py +++ b/lib/charms/operator_libs_linux/v0/apt.py @@ -1,4 +1,4 @@ -# Copyright 2025 Canonical Ltd. +# Copyright 2026 Canonical Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/charms/operator_libs_linux/v0/passwd.py b/lib/charms/operator_libs_linux/v0/passwd.py index 12636ed4..2543febe 100644 --- a/lib/charms/operator_libs_linux/v0/passwd.py +++ b/lib/charms/operator_libs_linux/v0/passwd.py @@ -1,4 +1,4 @@ -# Copyright 2025 Canonical Ltd. +# Copyright 2026 Canonical Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/charms/operator_libs_linux/v1/systemd.py b/lib/charms/operator_libs_linux/v1/systemd.py index 12758101..7b0093f0 100644 --- a/lib/charms/operator_libs_linux/v1/systemd.py +++ b/lib/charms/operator_libs_linux/v1/systemd.py @@ -1,4 +1,4 @@ -# Copyright 2025 Canonical Ltd. +# Copyright 2026 Canonical Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/charms/smtp_integrator/v0/smtp.py b/lib/charms/smtp_integrator/v0/smtp.py index 8ab7f285..04063800 100644 --- a/lib/charms/smtp_integrator/v0/smtp.py +++ b/lib/charms/smtp_integrator/v0/smtp.py @@ -1,4 +1,4 @@ -# Copyright 2025 Canonical Ltd. +# Copyright 2026 Canonical Ltd. # Licensed under the Apache2.0. See LICENSE file in charm source for details. """Library to manage the integration with the SMTP Integrator charm. diff --git a/metadata.yaml b/metadata.yaml index 74aa6d7b..204f69cb 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -1,4 +1,4 @@ -# Copyright 2025 Canonical Ltd +# Copyright 2026 Canonical Ltd # See LICENSE file for licensing details. # For a complete list of supported options, see: diff --git a/src/charm.py b/src/charm.py index 04cefc52..f9150f8b 100755 --- a/src/charm.py +++ b/src/charm.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Canonical Ltd +# Copyright 2026 Canonical Ltd # See LICENSE file for licensing details. # # Learn more at: https://juju.is/docs/sdk @@ -431,9 +431,9 @@ def _on_config_changed(self, _) -> None: self._set_ports() # Update additional configuration - update_service_conf( - {"global": {"deployment-mode": self.charm_config.deployment_mode}} - ) + update_service_conf({ + "global": {"deployment-mode": self.charm_config.deployment_mode} + }) configure_for_deployment_mode(self.charm_config.deployment_mode) write_deployment_mode_systemd_override(self.charm_config.deployment_mode) @@ -522,9 +522,9 @@ def _on_config_changed(self, _) -> None: logger.info("Generating new random cookie encryption key") cookie_encryption_key = generate_cookie_encryption_key() peer_relation = self.model.get_relation("replicas") - peer_relation.data[self.app].update( - {"cookie-encryption-key": cookie_encryption_key} - ) + peer_relation.data[self.app].update({ + "cookie-encryption-key": cookie_encryption_key + }) if (secret_token) and (secret_token != self._stored.secret_token): self._write_secret_token(secret_token) @@ -633,15 +633,13 @@ def _on_install(self, event: InstallEvent) -> None: if self.charm_config.min_install: logger.info("Not installing hashids..") - check_call( - [ - "apt", - "install", - LANDSCAPE_SERVER, - "--no-install-recommends", - "-y", - ] - ) + check_call([ + "apt", + "install", + LANDSCAPE_SERVER, + "--no-install-recommends", + "-y", + ]) else: # Explicitly ensure cache is up-to-date after adding the PPA. apt.add_package( @@ -705,23 +703,19 @@ def _start_services(self) -> bool: deployment_mode = self.charm_config.deployment_mode is_standalone = deployment_mode == "standalone" - update_default_settings( - { - "RUN_ALL": "no", - "RUN_APISERVER": str(self.charm_config.worker_counts), - "RUN_ASYNC_FRONTEND": "yes", - "RUN_JOBHANDLER": "yes", - "RUN_APPSERVER": str(self.charm_config.worker_counts), - "RUN_MSGSERVER": str(self.charm_config.worker_counts), - "RUN_PINGSERVER": str(self.charm_config.worker_counts), - "RUN_CRON": "yes" if is_leader else "no", - "RUN_PACKAGESEARCH": "yes" if is_leader else "no", - "RUN_PACKAGEUPLOADSERVER": ( - "yes" if is_leader and is_standalone else "no" - ), - "RUN_PPPA_PROXY": "no", - } - ) + update_default_settings({ + "RUN_ALL": "no", + "RUN_APISERVER": str(self.charm_config.worker_counts), + "RUN_ASYNC_FRONTEND": "yes", + "RUN_JOBHANDLER": "yes", + "RUN_APPSERVER": str(self.charm_config.worker_counts), + "RUN_MSGSERVER": str(self.charm_config.worker_counts), + "RUN_PINGSERVER": str(self.charm_config.worker_counts), + "RUN_CRON": "yes" if is_leader else "no", + "RUN_PACKAGESEARCH": "yes" if is_leader else "no", + "RUN_PACKAGEUPLOADSERVER": ("yes" if is_leader and is_standalone else "no"), + "RUN_PPPA_PROXY": "no", + }) logger.info("Starting services") @@ -1005,12 +999,10 @@ def _amqp_relation_joined(self, event: RelationJoinedEvent) -> None: self._stored.ready[relation_name] = False self.unit.status = MaintenanceStatus(f"Setting up {relation_name} connection") - event.relation.data[self.unit].update( - { - "username": AMQP_USERNAME, - "vhost": VHOSTS[relation_name], - } - ) + event.relation.data[self.unit].update({ + "username": AMQP_USERNAME, + "vhost": VHOSTS[relation_name], + }) def _amqp_relation_changed(self, event): unit_data = event.relation.data[event.unit] @@ -1037,14 +1029,12 @@ def _amqp_relation_changed(self, event): ) return - update_service_conf( - { - "broker": { - "host": hostname, - "password": password, - } + update_service_conf({ + "broker": { + "host": hostname, + "password": password, } - ) + }) self.unit.status = ActiveStatus("Unit is ready") self._update_ready_status() @@ -1215,11 +1205,9 @@ def _update_nrpe_checks(self, relation: Relation): }, } - relation.data[self.unit].update( - { - "monitors": yaml.safe_dump(monitors), - } - ) + relation.data[self.unit].update({ + "monitors": yaml.safe_dump(monitors), + }) if not os.path.exists(NRPE_D_DIR): logger.debug("NRPE directories not ready") @@ -1276,15 +1264,13 @@ def _application_dashboard_relation_joined(self, event: RelationJoinedEvent): else: icon_data = None - event.relation.data[self.app].update( - { - "name": "Landscape", - "url": root_url, - "subtitle": subtitle, - "group": group, - "icon": icon_data, - } - ) + event.relation.data[self.app].update({ + "name": "Landscape", + "url": root_url, + "subtitle": subtitle, + "group": group, + "icon": icon_data, + }) def _leader_elected(self, event: LeaderElectedEvent) -> None: # Just because we received this event does not mean we are @@ -1297,13 +1283,11 @@ def _leader_elected(self, event: LeaderElectedEvent) -> None: ip = str(self.model.get_binding(peer_relation).network.bind_address) peer_relation.data[self.app].update({"leader-ip": ip}) - update_service_conf( - { - "package-search": { - "host": "localhost", - }, - } - ) + update_service_conf({ + "package-search": { + "host": "localhost", + }, + }) self._leader_changed() @@ -1320,13 +1304,11 @@ def _leader_settings_changed(self, event: LeaderSettingsChangedEvent) -> None: leader_ip = peer_relation.data[self.app].get("leader-ip") if leader_ip: - update_service_conf( - { - "package-search": { - "host": leader_ip, - }, - } - ) + update_service_conf({ + "package-search": { + "host": leader_ip, + }, + }) self._leader_changed() @@ -1378,13 +1360,11 @@ def _on_replicas_relation_changed(self, event: RelationChangedEvent) -> None: if not self.unit.is_leader(): if leader_ip_value: - update_service_conf( - { - "package-search": { - "host": leader_ip_value, - }, - } - ) + update_service_conf({ + "package-search": { + "host": leader_ip_value, + }, + }) self._leader_changed() @@ -1497,14 +1477,12 @@ def _configure_openid(self) -> None: return self.unit.status = MaintenanceStatus("Configuring OpenID") - update_service_conf( - { - "landscape": { - "openid-provider-url": self.charm_config.openid_provider_url, - "openid-logout-url": self.charm_config.openid_logout_url, - }, - } - ) + update_service_conf({ + "landscape": { + "openid-provider-url": self.charm_config.openid_provider_url, + "openid-logout-url": self.charm_config.openid_logout_url, + }, + }) self.unit.status = WaitingStatus("Waiting on relations") def _bootstrap_account(self): diff --git a/src/settings_files.py b/src/settings_files.py index 82773c14..d5636433 100644 --- a/src/settings_files.py +++ b/src/settings_files.py @@ -1,4 +1,4 @@ -# Copyright 2025 Canonical Ltd +# Copyright 2026 Canonical Ltd """ Functions for manipulating Landscape Server service settings in the diff --git a/terraform/.tflint.hcl b/terraform/.tflint.hcl index ee21740d..5668c825 100644 --- a/terraform/.tflint.hcl +++ b/terraform/.tflint.hcl @@ -1,4 +1,4 @@ -# © 2025 Canonical Ltd. +# © 2026 Canonical Ltd. rule "terraform_required_version" { enabled = true diff --git a/terraform/charm/main.tf b/terraform/charm/main.tf index 6b30bc52..f56ef8bd 100644 --- a/terraform/charm/main.tf +++ b/terraform/charm/main.tf @@ -1,4 +1,4 @@ -# © 2025 Canonical Ltd. +# © 2026 Canonical Ltd. resource "juju_application" "landscape_server" { name = var.app_name diff --git a/terraform/charm/tests/main.tftest.hcl b/terraform/charm/tests/main.tftest.hcl index 0bdffb1c..64f980b0 100644 --- a/terraform/charm/tests/main.tftest.hcl +++ b/terraform/charm/tests/main.tftest.hcl @@ -1,4 +1,4 @@ -# © 2025 Canonical Ltd. +# © 2026 Canonical Ltd. run "setup_tests" { module { diff --git a/terraform/charm/tests/outputs.tftest.hcl b/terraform/charm/tests/outputs.tftest.hcl index 959bd3c3..d8e770b2 100644 --- a/terraform/charm/tests/outputs.tftest.hcl +++ b/terraform/charm/tests/outputs.tftest.hcl @@ -1,4 +1,4 @@ -# © 2025 Canonical Ltd. +# © 2026 Canonical Ltd. mock_provider "juju" {} diff --git a/terraform/charm/tests/setup/main.tf b/terraform/charm/tests/setup/main.tf index 56abf817..65589272 100644 --- a/terraform/charm/tests/setup/main.tf +++ b/terraform/charm/tests/setup/main.tf @@ -1,4 +1,4 @@ -# © 2025 Canonical Ltd. +# © 2026 Canonical Ltd. terraform { required_version = "~> 1.12" diff --git a/terraform/charm/variables.tf b/terraform/charm/variables.tf index 7933f839..c7813221 100644 --- a/terraform/charm/variables.tf +++ b/terraform/charm/variables.tf @@ -1,4 +1,4 @@ -# © 2025 Canonical Ltd. +# © 2026 Canonical Ltd. variable "app_name" { description = "Name of the application in the Juju model." diff --git a/terraform/charm/versions.tf b/terraform/charm/versions.tf index fbf26ab1..2ed5732e 100644 --- a/terraform/charm/versions.tf +++ b/terraform/charm/versions.tf @@ -1,4 +1,4 @@ -# © 2025 Canonical Ltd. +# © 2026 Canonical Ltd. terraform { required_version = "~> 1.12" diff --git a/terraform/product/modules/landscape-scalable/outputs.tf b/terraform/product/modules/landscape-scalable/outputs.tf index a6c87d75..517116f4 100644 --- a/terraform/product/modules/landscape-scalable/outputs.tf +++ b/terraform/product/modules/landscape-scalable/outputs.tf @@ -1,4 +1,4 @@ -# © 2025 Canonical Ltd. +# © 2026 Canonical Ltd. output "registration_key" { description = "Registration key from the Landscape Server config." diff --git a/terraform/product/modules/landscape-scalable/tests/defaults.tftest.hcl b/terraform/product/modules/landscape-scalable/tests/defaults.tftest.hcl index 8352e853..cadfb21a 100644 --- a/terraform/product/modules/landscape-scalable/tests/defaults.tftest.hcl +++ b/terraform/product/modules/landscape-scalable/tests/defaults.tftest.hcl @@ -1,4 +1,4 @@ -# © 2025 Canonical Ltd. +# © 2026 Canonical Ltd. mock_provider "juju" {} diff --git a/terraform/product/modules/landscape-scalable/tests/integrations.tftest.hcl b/terraform/product/modules/landscape-scalable/tests/integrations.tftest.hcl index a71962d0..54079b0e 100644 --- a/terraform/product/modules/landscape-scalable/tests/integrations.tftest.hcl +++ b/terraform/product/modules/landscape-scalable/tests/integrations.tftest.hcl @@ -1,4 +1,4 @@ -# © 2025 Canonical Ltd. +# © 2026 Canonical Ltd. mock_provider "juju" {} diff --git a/terraform/product/modules/landscape-scalable/tests/main.tftest.hcl b/terraform/product/modules/landscape-scalable/tests/main.tftest.hcl index 98758345..6448b5bd 100644 --- a/terraform/product/modules/landscape-scalable/tests/main.tftest.hcl +++ b/terraform/product/modules/landscape-scalable/tests/main.tftest.hcl @@ -1,4 +1,4 @@ -# © 2025 Canonical Ltd. +# © 2026 Canonical Ltd. run "setup_tests" { module { diff --git a/terraform/product/modules/landscape-scalable/tests/outputs.tftest.hcl b/terraform/product/modules/landscape-scalable/tests/outputs.tftest.hcl index 6602d979..62c81443 100644 --- a/terraform/product/modules/landscape-scalable/tests/outputs.tftest.hcl +++ b/terraform/product/modules/landscape-scalable/tests/outputs.tftest.hcl @@ -1,4 +1,4 @@ -# © 2025 Canonical Ltd. +# © 2026 Canonical Ltd. mock_provider "juju" {} diff --git a/terraform/product/modules/landscape-scalable/tests/setup/main.tf b/terraform/product/modules/landscape-scalable/tests/setup/main.tf index 56abf817..65589272 100644 --- a/terraform/product/modules/landscape-scalable/tests/setup/main.tf +++ b/terraform/product/modules/landscape-scalable/tests/setup/main.tf @@ -1,4 +1,4 @@ -# © 2025 Canonical Ltd. +# © 2026 Canonical Ltd. terraform { required_version = "~> 1.12" diff --git a/terraform/product/modules/landscape-scalable/versions.tf b/terraform/product/modules/landscape-scalable/versions.tf index fbf26ab1..2ed5732e 100644 --- a/terraform/product/modules/landscape-scalable/versions.tf +++ b/terraform/product/modules/landscape-scalable/versions.tf @@ -1,4 +1,4 @@ -# © 2025 Canonical Ltd. +# © 2026 Canonical Ltd. terraform { required_version = "~> 1.12" diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 4662435f..07bd6ad4 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -1,4 +1,4 @@ -# Copyright 2025 Canonical Ltd +# Copyright 2026 Canonical Ltd # See LICENSE file for licensing details. # # Learn more about testing at @@ -1007,9 +1007,9 @@ def test_install_license_file_b64(self): def test_update_ready_status_not_running(self): self.harness.charm.unit.status = WaitingStatus() - self.harness.charm._stored.ready.update( - {k: True for k in self.harness.charm._stored.ready.keys()} - ) + self.harness.charm._stored.ready.update({ + k: True for k in self.harness.charm._stored.ready.keys() + }) patches = patch.multiple( "charm", @@ -1031,9 +1031,9 @@ def test_update_ready_status_not_running(self): def test_update_ready_status_running(self): self.harness.charm.unit.status = WaitingStatus() - self.harness.charm._stored.ready.update( - {k: True for k in self.harness.charm._stored.ready.keys()} - ) + self.harness.charm._stored.ready.update({ + k: True for k in self.harness.charm._stored.ready.keys() + }) self.harness.charm._stored.running = True self.harness.charm._update_ready_status() @@ -1045,9 +1045,9 @@ def test_update_ready_status_running(self): def test_update_ready_status_called_process_error(self): self.harness.charm.unit.status = WaitingStatus() - self.harness.charm._stored.ready.update( - {k: True for k in self.harness.charm._stored.ready.keys()} - ) + self.harness.charm._stored.ready.update({ + k: True for k in self.harness.charm._stored.ready.keys() + }) patches = patch.multiple( "charm", @@ -1116,18 +1116,16 @@ def test_db_relation_changed_called_process_error(self): self.assertIsInstance(status, BlockedStatus) self.assertFalse(self.harness.charm._stored.ready["db"]) - update_service_conf_mock.assert_called_once_with( - { - "stores": { - "host": "1.2.3.4:5678", - "password": "testpass", - }, - "schema": { - "store_user": "testuser", - "store_password": "testpass", - }, - } - ) + update_service_conf_mock.assert_called_once_with({ + "stores": { + "host": "1.2.3.4:5678", + "password": "testpass", + }, + "schema": { + "store_user": "testuser", + "store_password": "testpass", + }, + }) @patch("charm.update_service_conf") def test_on_manual_db_config_change(self, _): @@ -1164,13 +1162,11 @@ def test_on_manual_db_config_change(self, _): self.assertEqual(update_service_conf_mock.call_count, 2) self.assertEqual( update_service_conf_mock.call_args_list[1], - call( - { - "stores": { - "host": "hello:world", - }, - } - ), + call({ + "stores": { + "host": "hello:world", + }, + }), ) @patch("charm.update_service_conf") @@ -1352,14 +1348,12 @@ def test_amqp_relation_changed(self): self.assertTrue(self.harness.charm._stored.ready["inbound-amqp"]) self.assertTrue(self.harness.charm._stored.ready["outbound-amqp"]) - mock_update_conf.assert_called_once_with( - { - "broker": { - "host": ",".join(hostname), - "password": password, - }, - } - ) + mock_update_conf.assert_called_once_with({ + "broker": { + "host": ",".join(hostname), + "password": password, + }, + }) def test_amqp_relation_changed_outbound_first(self): """ @@ -1396,14 +1390,12 @@ def test_amqp_relation_changed_outbound_first(self): self.assertTrue(self.harness.charm._stored.ready["inbound-amqp"]) self.assertTrue(self.harness.charm._stored.ready["outbound-amqp"]) - mock_update_conf.assert_called_once_with( - { - "broker": { - "host": hostname, - "password": password, - }, - } - ) + mock_update_conf.assert_called_once_with({ + "broker": { + "host": hostname, + "password": password, + }, + }) def test_configure_smtp_relay_host(self): mock_postfix_cf = os.path.join(self.tempdir.name, "my_postfix.cf") @@ -1779,13 +1771,11 @@ def test_on_replicas_relation_changed_leader(self): ) self.harness.charm._update_nrpe_checks.assert_called_once() - mock_update_conf.assert_called_once_with( - { - "package-search": { - "host": "localhost", - }, - } - ) + mock_update_conf.assert_called_once_with({ + "package-search": { + "host": "localhost", + }, + }) def test_on_replicas_relation_changed_non_leader(self): """ @@ -1805,13 +1795,11 @@ def test_on_replicas_relation_changed_non_leader(self): ) self.harness.charm._update_nrpe_checks.assert_called_once() - mock_update_conf.assert_called_once_with( - { - "package-search": { - "host": "test", - }, - } - ) + mock_update_conf.assert_called_once_with({ + "package-search": { + "host": "test", + }, + }) class TestMultiplePPAs: @@ -1916,39 +1904,33 @@ def setUp(self): @patch("charm.update_service_conf") def test_bootstrap_account_doesnt_run_with_missing_configs(self, _): - self.harness.update_config( - { - "admin_email": "hello@ubuntu.com", - "admin_name": "Hello Ubuntu", - } - ) + self.harness.update_config({ + "admin_email": "hello@ubuntu.com", + "admin_name": "Hello Ubuntu", + }) self.assertIn("password required", self.log_mock.call_args.args[0]) self.process_mock.assert_not_called() @patch("charm.update_service_conf") def test_bootstrap_account_password_redacted(self, _): - self.harness.update_config( - { - "admin_email": "hello@ubuntu.com", - "admin_name": "Hello Ubuntu", - "admin_password": "secret123", - "registration_key": "secret123", - "root_url": "https://www.landscape.com", - } - ) + self.harness.update_config({ + "admin_email": "hello@ubuntu.com", + "admin_name": "Hello Ubuntu", + "admin_password": "secret123", + "registration_key": "secret123", + "root_url": "https://www.landscape.com", + }) for mock_call in self.log_info_mock.call_args_list: self.assertNotIn("secret123", str(mock_call.args)) @patch("charm.update_service_conf") def test_bootstrap_account_uses_leader_ip_when_no_root_url(self, _): self.harness.charm._stored.leader_ip = "10.0.0.1" - self.harness.update_config( - { - "admin_email": "hello@ubuntu.com", - "admin_name": "Hello Ubuntu", - "admin_password": "password", - } - ) + self.harness.update_config({ + "admin_email": "hello@ubuntu.com", + "admin_name": "Hello Ubuntu", + "admin_password": "password", + }) self.assertIn( "https://10.0.0.1", self.process_mock.call_args.args[0], @@ -1959,14 +1941,12 @@ def test_bootstrap_account_config_url_over_leader_ip(self, _): """If config root_url and leader_ip are both set, use config url""" self.harness.charm._stored.leader_ip = "10.0.0.1" config_root_url = "https://www.landscape.com" - self.harness.update_config( - { - "admin_email": "hello@ubuntu.com", - "admin_name": "Hello Ubuntu", - "admin_password": "password", - "root_url": config_root_url, - } - ) + self.harness.update_config({ + "admin_email": "hello@ubuntu.com", + "admin_name": "Hello Ubuntu", + "admin_password": "password", + "root_url": config_root_url, + }) self.assertIn(config_root_url, self.process_mock.call_args.args[0]) @patch("charm.update_service_conf") diff --git a/tests/unit/test_settings_files.py b/tests/unit/test_settings_files.py index fe526290..4c692165 100644 --- a/tests/unit/test_settings_files.py +++ b/tests/unit/test_settings_files.py @@ -1,4 +1,4 @@ -# Copyright 2025 Canonical Ltd +# Copyright 2026 Canonical Ltd from base64 import b64encode from io import BytesIO, StringIO From 9f4093afde6b4d60c27be24d9051a676715cf274 Mon Sep 17 00:00:00 2001 From: jansdhillon Date: Fri, 5 Jun 2026 11:28:01 -0600 Subject: [PATCH 05/11] dont change license for charmlibs --- lib/charms/grafana_agent/LICENSE | 2 +- lib/charms/grafana_agent/v0/cos_agent.py | 2 +- lib/charms/operator_libs_linux/v0/apt.py | 2 +- lib/charms/operator_libs_linux/v0/passwd.py | 2 +- lib/charms/operator_libs_linux/v1/systemd.py | 2 +- lib/charms/smtp_integrator/v0/smtp.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/charms/grafana_agent/LICENSE b/lib/charms/grafana_agent/LICENSE index 4fe1d7e2..5823fae5 100644 --- a/lib/charms/grafana_agent/LICENSE +++ b/lib/charms/grafana_agent/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2026 Canonical Ltd. + Copyright 2025 Canonical Ltd. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/lib/charms/grafana_agent/v0/cos_agent.py b/lib/charms/grafana_agent/v0/cos_agent.py index 366f4853..bbc894bf 100644 --- a/lib/charms/grafana_agent/v0/cos_agent.py +++ b/lib/charms/grafana_agent/v0/cos_agent.py @@ -1,4 +1,4 @@ -# Copyright 2026 Canonical Ltd. +# Copyright 2025 Canonical Ltd. # See LICENSE file for licensing details. r"""## Overview. diff --git a/lib/charms/operator_libs_linux/v0/apt.py b/lib/charms/operator_libs_linux/v0/apt.py index 17e71e02..cb09f6f4 100644 --- a/lib/charms/operator_libs_linux/v0/apt.py +++ b/lib/charms/operator_libs_linux/v0/apt.py @@ -1,4 +1,4 @@ -# Copyright 2026 Canonical Ltd. +# Copyright 2025 Canonical Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/charms/operator_libs_linux/v0/passwd.py b/lib/charms/operator_libs_linux/v0/passwd.py index 2543febe..12636ed4 100644 --- a/lib/charms/operator_libs_linux/v0/passwd.py +++ b/lib/charms/operator_libs_linux/v0/passwd.py @@ -1,4 +1,4 @@ -# Copyright 2026 Canonical Ltd. +# Copyright 2025 Canonical Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/charms/operator_libs_linux/v1/systemd.py b/lib/charms/operator_libs_linux/v1/systemd.py index 7b0093f0..12758101 100644 --- a/lib/charms/operator_libs_linux/v1/systemd.py +++ b/lib/charms/operator_libs_linux/v1/systemd.py @@ -1,4 +1,4 @@ -# Copyright 2026 Canonical Ltd. +# Copyright 2025 Canonical Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/charms/smtp_integrator/v0/smtp.py b/lib/charms/smtp_integrator/v0/smtp.py index 04063800..8ab7f285 100644 --- a/lib/charms/smtp_integrator/v0/smtp.py +++ b/lib/charms/smtp_integrator/v0/smtp.py @@ -1,4 +1,4 @@ -# Copyright 2026 Canonical Ltd. +# Copyright 2025 Canonical Ltd. # Licensed under the Apache2.0. See LICENSE file in charm source for details. """Library to manage the integration with the SMTP Integrator charm. From fce7dab2d85ccbbfe36cce9b35cbb355e56ae5e0 Mon Sep 17 00:00:00 2001 From: jansdhillon Date: Fri, 5 Jun 2026 11:34:52 -0600 Subject: [PATCH 06/11] lint --- src/charm.py | 154 +++++++++++++++++++++----------------- tests/unit/test_charm.py | 156 ++++++++++++++++++++++----------------- 2 files changed, 176 insertions(+), 134 deletions(-) diff --git a/src/charm.py b/src/charm.py index f9150f8b..69c51fb0 100755 --- a/src/charm.py +++ b/src/charm.py @@ -431,9 +431,9 @@ def _on_config_changed(self, _) -> None: self._set_ports() # Update additional configuration - update_service_conf({ - "global": {"deployment-mode": self.charm_config.deployment_mode} - }) + update_service_conf( + {"global": {"deployment-mode": self.charm_config.deployment_mode}} + ) configure_for_deployment_mode(self.charm_config.deployment_mode) write_deployment_mode_systemd_override(self.charm_config.deployment_mode) @@ -522,9 +522,9 @@ def _on_config_changed(self, _) -> None: logger.info("Generating new random cookie encryption key") cookie_encryption_key = generate_cookie_encryption_key() peer_relation = self.model.get_relation("replicas") - peer_relation.data[self.app].update({ - "cookie-encryption-key": cookie_encryption_key - }) + peer_relation.data[self.app].update( + {"cookie-encryption-key": cookie_encryption_key} + ) if (secret_token) and (secret_token != self._stored.secret_token): self._write_secret_token(secret_token) @@ -633,13 +633,15 @@ def _on_install(self, event: InstallEvent) -> None: if self.charm_config.min_install: logger.info("Not installing hashids..") - check_call([ - "apt", - "install", - LANDSCAPE_SERVER, - "--no-install-recommends", - "-y", - ]) + check_call( + [ + "apt", + "install", + LANDSCAPE_SERVER, + "--no-install-recommends", + "-y", + ] + ) else: # Explicitly ensure cache is up-to-date after adding the PPA. apt.add_package( @@ -703,19 +705,23 @@ def _start_services(self) -> bool: deployment_mode = self.charm_config.deployment_mode is_standalone = deployment_mode == "standalone" - update_default_settings({ - "RUN_ALL": "no", - "RUN_APISERVER": str(self.charm_config.worker_counts), - "RUN_ASYNC_FRONTEND": "yes", - "RUN_JOBHANDLER": "yes", - "RUN_APPSERVER": str(self.charm_config.worker_counts), - "RUN_MSGSERVER": str(self.charm_config.worker_counts), - "RUN_PINGSERVER": str(self.charm_config.worker_counts), - "RUN_CRON": "yes" if is_leader else "no", - "RUN_PACKAGESEARCH": "yes" if is_leader else "no", - "RUN_PACKAGEUPLOADSERVER": ("yes" if is_leader and is_standalone else "no"), - "RUN_PPPA_PROXY": "no", - }) + update_default_settings( + { + "RUN_ALL": "no", + "RUN_APISERVER": str(self.charm_config.worker_counts), + "RUN_ASYNC_FRONTEND": "yes", + "RUN_JOBHANDLER": "yes", + "RUN_APPSERVER": str(self.charm_config.worker_counts), + "RUN_MSGSERVER": str(self.charm_config.worker_counts), + "RUN_PINGSERVER": str(self.charm_config.worker_counts), + "RUN_CRON": "yes" if is_leader else "no", + "RUN_PACKAGESEARCH": "yes" if is_leader else "no", + "RUN_PACKAGEUPLOADSERVER": ( + "yes" if is_leader and is_standalone else "no" + ), + "RUN_PPPA_PROXY": "no", + } + ) logger.info("Starting services") @@ -999,10 +1005,12 @@ def _amqp_relation_joined(self, event: RelationJoinedEvent) -> None: self._stored.ready[relation_name] = False self.unit.status = MaintenanceStatus(f"Setting up {relation_name} connection") - event.relation.data[self.unit].update({ - "username": AMQP_USERNAME, - "vhost": VHOSTS[relation_name], - }) + event.relation.data[self.unit].update( + { + "username": AMQP_USERNAME, + "vhost": VHOSTS[relation_name], + } + ) def _amqp_relation_changed(self, event): unit_data = event.relation.data[event.unit] @@ -1029,12 +1037,14 @@ def _amqp_relation_changed(self, event): ) return - update_service_conf({ - "broker": { - "host": hostname, - "password": password, + update_service_conf( + { + "broker": { + "host": hostname, + "password": password, + } } - }) + ) self.unit.status = ActiveStatus("Unit is ready") self._update_ready_status() @@ -1205,9 +1215,11 @@ def _update_nrpe_checks(self, relation: Relation): }, } - relation.data[self.unit].update({ - "monitors": yaml.safe_dump(monitors), - }) + relation.data[self.unit].update( + { + "monitors": yaml.safe_dump(monitors), + } + ) if not os.path.exists(NRPE_D_DIR): logger.debug("NRPE directories not ready") @@ -1264,13 +1276,15 @@ def _application_dashboard_relation_joined(self, event: RelationJoinedEvent): else: icon_data = None - event.relation.data[self.app].update({ - "name": "Landscape", - "url": root_url, - "subtitle": subtitle, - "group": group, - "icon": icon_data, - }) + event.relation.data[self.app].update( + { + "name": "Landscape", + "url": root_url, + "subtitle": subtitle, + "group": group, + "icon": icon_data, + } + ) def _leader_elected(self, event: LeaderElectedEvent) -> None: # Just because we received this event does not mean we are @@ -1283,11 +1297,13 @@ def _leader_elected(self, event: LeaderElectedEvent) -> None: ip = str(self.model.get_binding(peer_relation).network.bind_address) peer_relation.data[self.app].update({"leader-ip": ip}) - update_service_conf({ - "package-search": { - "host": "localhost", - }, - }) + update_service_conf( + { + "package-search": { + "host": "localhost", + }, + } + ) self._leader_changed() @@ -1304,11 +1320,13 @@ def _leader_settings_changed(self, event: LeaderSettingsChangedEvent) -> None: leader_ip = peer_relation.data[self.app].get("leader-ip") if leader_ip: - update_service_conf({ - "package-search": { - "host": leader_ip, - }, - }) + update_service_conf( + { + "package-search": { + "host": leader_ip, + }, + } + ) self._leader_changed() @@ -1360,11 +1378,13 @@ def _on_replicas_relation_changed(self, event: RelationChangedEvent) -> None: if not self.unit.is_leader(): if leader_ip_value: - update_service_conf({ - "package-search": { - "host": leader_ip_value, - }, - }) + update_service_conf( + { + "package-search": { + "host": leader_ip_value, + }, + } + ) self._leader_changed() @@ -1477,12 +1497,14 @@ def _configure_openid(self) -> None: return self.unit.status = MaintenanceStatus("Configuring OpenID") - update_service_conf({ - "landscape": { - "openid-provider-url": self.charm_config.openid_provider_url, - "openid-logout-url": self.charm_config.openid_logout_url, - }, - }) + update_service_conf( + { + "landscape": { + "openid-provider-url": self.charm_config.openid_provider_url, + "openid-logout-url": self.charm_config.openid_logout_url, + }, + } + ) self.unit.status = WaitingStatus("Waiting on relations") def _bootstrap_account(self): diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 07bd6ad4..f28ceedd 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -1007,9 +1007,9 @@ def test_install_license_file_b64(self): def test_update_ready_status_not_running(self): self.harness.charm.unit.status = WaitingStatus() - self.harness.charm._stored.ready.update({ - k: True for k in self.harness.charm._stored.ready.keys() - }) + self.harness.charm._stored.ready.update( + {k: True for k in self.harness.charm._stored.ready.keys()} + ) patches = patch.multiple( "charm", @@ -1031,9 +1031,9 @@ def test_update_ready_status_not_running(self): def test_update_ready_status_running(self): self.harness.charm.unit.status = WaitingStatus() - self.harness.charm._stored.ready.update({ - k: True for k in self.harness.charm._stored.ready.keys() - }) + self.harness.charm._stored.ready.update( + {k: True for k in self.harness.charm._stored.ready.keys()} + ) self.harness.charm._stored.running = True self.harness.charm._update_ready_status() @@ -1045,9 +1045,9 @@ def test_update_ready_status_running(self): def test_update_ready_status_called_process_error(self): self.harness.charm.unit.status = WaitingStatus() - self.harness.charm._stored.ready.update({ - k: True for k in self.harness.charm._stored.ready.keys() - }) + self.harness.charm._stored.ready.update( + {k: True for k in self.harness.charm._stored.ready.keys()} + ) patches = patch.multiple( "charm", @@ -1116,16 +1116,18 @@ def test_db_relation_changed_called_process_error(self): self.assertIsInstance(status, BlockedStatus) self.assertFalse(self.harness.charm._stored.ready["db"]) - update_service_conf_mock.assert_called_once_with({ - "stores": { - "host": "1.2.3.4:5678", - "password": "testpass", - }, - "schema": { - "store_user": "testuser", - "store_password": "testpass", - }, - }) + update_service_conf_mock.assert_called_once_with( + { + "stores": { + "host": "1.2.3.4:5678", + "password": "testpass", + }, + "schema": { + "store_user": "testuser", + "store_password": "testpass", + }, + } + ) @patch("charm.update_service_conf") def test_on_manual_db_config_change(self, _): @@ -1162,11 +1164,13 @@ def test_on_manual_db_config_change(self, _): self.assertEqual(update_service_conf_mock.call_count, 2) self.assertEqual( update_service_conf_mock.call_args_list[1], - call({ - "stores": { - "host": "hello:world", - }, - }), + call( + { + "stores": { + "host": "hello:world", + }, + } + ), ) @patch("charm.update_service_conf") @@ -1348,12 +1352,14 @@ def test_amqp_relation_changed(self): self.assertTrue(self.harness.charm._stored.ready["inbound-amqp"]) self.assertTrue(self.harness.charm._stored.ready["outbound-amqp"]) - mock_update_conf.assert_called_once_with({ - "broker": { - "host": ",".join(hostname), - "password": password, - }, - }) + mock_update_conf.assert_called_once_with( + { + "broker": { + "host": ",".join(hostname), + "password": password, + }, + } + ) def test_amqp_relation_changed_outbound_first(self): """ @@ -1390,12 +1396,14 @@ def test_amqp_relation_changed_outbound_first(self): self.assertTrue(self.harness.charm._stored.ready["inbound-amqp"]) self.assertTrue(self.harness.charm._stored.ready["outbound-amqp"]) - mock_update_conf.assert_called_once_with({ - "broker": { - "host": hostname, - "password": password, - }, - }) + mock_update_conf.assert_called_once_with( + { + "broker": { + "host": hostname, + "password": password, + }, + } + ) def test_configure_smtp_relay_host(self): mock_postfix_cf = os.path.join(self.tempdir.name, "my_postfix.cf") @@ -1771,11 +1779,13 @@ def test_on_replicas_relation_changed_leader(self): ) self.harness.charm._update_nrpe_checks.assert_called_once() - mock_update_conf.assert_called_once_with({ - "package-search": { - "host": "localhost", - }, - }) + mock_update_conf.assert_called_once_with( + { + "package-search": { + "host": "localhost", + }, + } + ) def test_on_replicas_relation_changed_non_leader(self): """ @@ -1795,11 +1805,13 @@ def test_on_replicas_relation_changed_non_leader(self): ) self.harness.charm._update_nrpe_checks.assert_called_once() - mock_update_conf.assert_called_once_with({ - "package-search": { - "host": "test", - }, - }) + mock_update_conf.assert_called_once_with( + { + "package-search": { + "host": "test", + }, + } + ) class TestMultiplePPAs: @@ -1904,33 +1916,39 @@ def setUp(self): @patch("charm.update_service_conf") def test_bootstrap_account_doesnt_run_with_missing_configs(self, _): - self.harness.update_config({ - "admin_email": "hello@ubuntu.com", - "admin_name": "Hello Ubuntu", - }) + self.harness.update_config( + { + "admin_email": "hello@ubuntu.com", + "admin_name": "Hello Ubuntu", + } + ) self.assertIn("password required", self.log_mock.call_args.args[0]) self.process_mock.assert_not_called() @patch("charm.update_service_conf") def test_bootstrap_account_password_redacted(self, _): - self.harness.update_config({ - "admin_email": "hello@ubuntu.com", - "admin_name": "Hello Ubuntu", - "admin_password": "secret123", - "registration_key": "secret123", - "root_url": "https://www.landscape.com", - }) + self.harness.update_config( + { + "admin_email": "hello@ubuntu.com", + "admin_name": "Hello Ubuntu", + "admin_password": "secret123", + "registration_key": "secret123", + "root_url": "https://www.landscape.com", + } + ) for mock_call in self.log_info_mock.call_args_list: self.assertNotIn("secret123", str(mock_call.args)) @patch("charm.update_service_conf") def test_bootstrap_account_uses_leader_ip_when_no_root_url(self, _): self.harness.charm._stored.leader_ip = "10.0.0.1" - self.harness.update_config({ - "admin_email": "hello@ubuntu.com", - "admin_name": "Hello Ubuntu", - "admin_password": "password", - }) + self.harness.update_config( + { + "admin_email": "hello@ubuntu.com", + "admin_name": "Hello Ubuntu", + "admin_password": "password", + } + ) self.assertIn( "https://10.0.0.1", self.process_mock.call_args.args[0], @@ -1941,12 +1959,14 @@ def test_bootstrap_account_config_url_over_leader_ip(self, _): """If config root_url and leader_ip are both set, use config url""" self.harness.charm._stored.leader_ip = "10.0.0.1" config_root_url = "https://www.landscape.com" - self.harness.update_config({ - "admin_email": "hello@ubuntu.com", - "admin_name": "Hello Ubuntu", - "admin_password": "password", - "root_url": config_root_url, - }) + self.harness.update_config( + { + "admin_email": "hello@ubuntu.com", + "admin_name": "Hello Ubuntu", + "admin_password": "password", + "root_url": config_root_url, + } + ) self.assertIn(config_root_url, self.process_mock.call_args.args[0]) @patch("charm.update_service_conf") From fb8a5a7d60ab8b23e9fd0973c5ff912155a3b2ec Mon Sep 17 00:00:00 2001 From: jansdhillon Date: Fri, 5 Jun 2026 11:38:55 -0600 Subject: [PATCH 07/11] dont run tftests on self hosted runner --- .github/workflows/test_terraform_modules.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test_terraform_modules.yaml b/.github/workflows/test_terraform_modules.yaml index a2759ef2..5331fef3 100644 --- a/.github/workflows/test_terraform_modules.yaml +++ b/.github/workflows/test_terraform_modules.yaml @@ -15,4 +15,5 @@ jobs: secrets: inherit with: lxd-controller: true + self-hosted-runner: true terraform-directories: '["terraform/charm", "terraform/product/modules/landscape-scalable"]' From ec83ae1b825b538e71ddea5c5ed275894ddfcfae Mon Sep 17 00:00:00 2001 From: jansdhillon Date: Fri, 5 Jun 2026 11:50:19 -0600 Subject: [PATCH 08/11] use false not true --- .github/workflows/test_terraform_modules.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_terraform_modules.yaml b/.github/workflows/test_terraform_modules.yaml index 5331fef3..71412876 100644 --- a/.github/workflows/test_terraform_modules.yaml +++ b/.github/workflows/test_terraform_modules.yaml @@ -15,5 +15,5 @@ jobs: secrets: inherit with: lxd-controller: true - self-hosted-runner: true + self-hosted-runner: false terraform-directories: '["terraform/charm", "terraform/product/modules/landscape-scalable"]' From f2f44ffb162a643f94ba5c2df65f109c1405db6b Mon Sep 17 00:00:00 2001 From: jansdhillon Date: Fri, 5 Jun 2026 11:52:13 -0600 Subject: [PATCH 09/11] okay i guess it's needed? --- .github/workflows/test_terraform_modules.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_terraform_modules.yaml b/.github/workflows/test_terraform_modules.yaml index 71412876..5331fef3 100644 --- a/.github/workflows/test_terraform_modules.yaml +++ b/.github/workflows/test_terraform_modules.yaml @@ -15,5 +15,5 @@ jobs: secrets: inherit with: lxd-controller: true - self-hosted-runner: false + self-hosted-runner: true terraform-directories: '["terraform/charm", "terraform/product/modules/landscape-scalable"]' From fd1185d7c2dd8dfc10ac0137330359186cf42d71 Mon Sep 17 00:00:00 2001 From: jansdhillon Date: Fri, 5 Jun 2026 13:05:53 -0600 Subject: [PATCH 10/11] fix: stop pinning outdated terraform test revision --- .../product/modules/landscape-scalable/tests/main.tftest.hcl | 2 -- 1 file changed, 2 deletions(-) diff --git a/terraform/product/modules/landscape-scalable/tests/main.tftest.hcl b/terraform/product/modules/landscape-scalable/tests/main.tftest.hcl index 6448b5bd..0e7b4459 100644 --- a/terraform/product/modules/landscape-scalable/tests/main.tftest.hcl +++ b/terraform/product/modules/landscape-scalable/tests/main.tftest.hcl @@ -11,8 +11,6 @@ run "basic_deploy" { model_uuid = run.setup_tests.model_uuid landscape_server = { channel = "latest/edge" - # renovate: depName="landscape-server" - revision = 143 } } From 6547f3d8fce1006cf53987fe52a90714003de6dc Mon Sep 17 00:00:00 2001 From: jansdhillon Date: Sun, 7 Jun 2026 18:54:55 -0600 Subject: [PATCH 11/11] make test run on 26.04/edge --- terraform/charm/tests/main.tftest.hcl | 4 ++-- .../product/modules/landscape-scalable/tests/main.tftest.hcl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/terraform/charm/tests/main.tftest.hcl b/terraform/charm/tests/main.tftest.hcl index 64f980b0..f9e8f61c 100644 --- a/terraform/charm/tests/main.tftest.hcl +++ b/terraform/charm/tests/main.tftest.hcl @@ -9,9 +9,9 @@ run "setup_tests" { run "basic_deploy" { variables { model_uuid = run.setup_tests.model_uuid - channel = "latest/edge" + channel = "26.04/edge" # renovate: depName="landscape-server" - revision = 143 + revision = 355 } assert { diff --git a/terraform/product/modules/landscape-scalable/tests/main.tftest.hcl b/terraform/product/modules/landscape-scalable/tests/main.tftest.hcl index 0e7b4459..801f563d 100644 --- a/terraform/product/modules/landscape-scalable/tests/main.tftest.hcl +++ b/terraform/product/modules/landscape-scalable/tests/main.tftest.hcl @@ -10,7 +10,7 @@ run "basic_deploy" { variables { model_uuid = run.setup_tests.model_uuid landscape_server = { - channel = "latest/edge" + channel = "26.04/edge" } }