From faf70a4178482b06d2243a81c49adbd1496a9025 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Wed, 2 Apr 2025 15:59:14 +0200 Subject: [PATCH 01/51] Add refresh v3 draft implementation --- actions.yaml | 60 ++++- charm_version | 1 - charmcraft.yaml | 12 +- config.yaml | 8 +- metadata.yaml | 7 +- poetry.lock | 196 ++++++++++----- pyproject.toml | 1 + refresh_versions.toml | 14 ++ src/abstract_charm.py | 231 ++++++++---------- src/charm.py | 115 ++++----- src/container.py | 34 ++- src/machine_upgrade.py | 198 --------------- src/machine_workload.py | 2 +- src/relations/cos.py | 13 +- src/relations/database_provides.py | 3 - src/relations/database_requires.py | 11 +- .../deprecated_shared_db_database_provides.py | 2 - src/relations/hacluster.py | 4 - src/snap.py | 173 ++++++++----- src/upgrade.py | 230 ----------------- src/workload.py | 83 +++++-- tests/unit/conftest.py | 2 +- tests/unit/test_workload.py | 2 +- workload_version | 1 - 24 files changed, 580 insertions(+), 823 deletions(-) delete mode 100644 charm_version create mode 100644 refresh_versions.toml delete mode 100644 src/machine_upgrade.py delete mode 100644 src/upgrade.py delete mode 100644 workload_version diff --git a/actions.yaml b/actions.yaml index 0bedd9b2..85144490 100644 --- a/actions.yaml +++ b/actions.yaml @@ -1,18 +1,60 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. -resume-upgrade: - description: Upgrade remaining units (after you manually verified that upgraded units are healthy). - -force-upgrade: +pre-refresh-check: + description: Check if charm is ready to refresh +force-refresh-start: description: | - Potential of *data loss* and *downtime* + Potential of data loss and downtime + + Force refresh of first unit + + Must run with at least one of the parameters `=false` + params: + check-compatibility: + type: boolean + default: true + description: | + Potential of data loss and downtime + + If `false`, force refresh if new version of MySQL Router and/or charm is not compatible with previous version + run-pre-refresh-checks: + type: boolean + default: true + description: | + Potential of data loss and downtime + + If `false`, force refresh if app is unhealthy or not ready to refresh (and unit status shows "Pre-refresh check failed") + check-workload-container: + type: boolean + default: true + description: | + Potential of data loss and downtime during and after refresh - Force upgrade of this unit. + If `false`, allow refresh to MySQL Router container version that has not been validated to work with the charm revision + required: [] +resume-refresh: + description: | + Refresh next unit(s) (after you have manually verified that refreshed units are healthy) + + If the `pause_after_unit_refresh` config is set to `all`, this action will refresh the next unit. + + If `pause_after_unit_refresh` is set to `first`, this action will refresh all remaining units. + Exception: if automatic health checks fail after a unit has refreshed, the refresh will pause. - Use to - - force incompatible upgrade and/or - - continue upgrade if 1+ upgraded units have non-active status + If `pause_after_unit_refresh` is set to `none`, this action will have no effect unless it is called with `check-health-of-refreshed-units` as `false`. + params: + check-health-of-refreshed-units: + type: boolean + default: true + description: | + Potential of data loss and downtime + + If `false`, force refresh (of next unit) if 1 or more refreshed units are unhealthy + + Warning: if first unit to refresh is unhealthy, consider running `force-refresh-start` action on that unit instead of using this parameter. + If first unit to refresh is unhealthy because compatibility checks, pre-refresh checks, or workload container checks are failing, this parameter is more destructive than the `force-refresh-start` action. + required: [] set-tls-private-key: description: diff --git a/charm_version b/charm_version deleted file mode 100644 index d00491fd..00000000 --- a/charm_version +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/charmcraft.yaml b/charmcraft.yaml index ee962e66..b0de9dd7 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -51,7 +51,7 @@ parts: source: . after: - poetry-deps - poetry-export-extra-args: ['--only', 'main,charm-libs'] + poetry-export-extra-args: ['--only', 'main,charm-libs', '--without-hashes'] # TODO: re-enable hashes build-packages: - libffi-dev # Needed to build Python dependencies with Rust from source - libssl-dev # Needed to build Python dependencies with Rust from source @@ -86,15 +86,9 @@ parts: build-packages: - git override-build: | - # Workaround to add unique identifier (git hash) to charm version while specification - # DA053 - Charm versioning - # (https://docs.google.com/document/d/1Jv1jhWLl8ejK3iJn7Q3VbCIM9GIhp8926bgXpdtx-Sg/edit?pli=1) - # is pending review. - python3 -c 'import pathlib; import shutil; import subprocess; git_hash=subprocess.run(["git", "describe", "--always", "--dirty"], capture_output=True, check=True, encoding="utf-8").stdout; file = pathlib.Path("charm_version"); shutil.copy(file, pathlib.Path("charm_version.backup")); version = file.read_text().strip(); file.write_text(f"{version}+{git_hash}")' - + # TODO: set charm version in refresh_versions.toml craftctl default stage: - LICENSE - - charm_version - - workload_version + - refresh_versions.toml - templates diff --git a/config.yaml b/config.yaml index d4fc8cdc..dfe2fe89 100644 --- a/config.yaml +++ b/config.yaml @@ -2,8 +2,14 @@ # See LICENSE file for licensing details. options: - vip: description: | Virtual IP to use to front mysql router units. Used only in case of external node connection. type: string + pause_after_unit_refresh: + description: | + Wait for manual confirmation to resume refresh after these units refresh + + Allowed values: "all", "first", "none" + type: string + default: first diff --git a/metadata.yaml b/metadata.yaml index 93f26668..9af31278 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -55,11 +55,8 @@ peers: interface: tls cos: interface: cos - upgrade-version-a: - # Relation versioning scheme: - # DA056 - Upgrading in-place upgrade protocol - # https://docs.google.com/document/d/1H7qy5SAwLiCOKO9xMQJbbQP5_-jGV6Lhi-mJOk4gZ08/edit - interface: upgrade + refresh-v-three: + interface: refresh # DEPRECATED shared-db: Workaround for legacy "mysql-shared" interface using unit databags instead of app databag deprecated-shared-db-credentials: interface: _deprecated_shared_db_peers diff --git a/poetry.lock b/poetry.lock index ae186651..1b6fc1ec 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "allure-pytest" @@ -54,7 +54,7 @@ version = "4.5.2" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.8" -groups = ["charm-libs"] +groups = ["main", "charm-libs"] files = [ {file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"}, {file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"}, @@ -291,6 +291,67 @@ markers = {charm-libs = "platform_python_implementation != \"PyPy\""} [package.dependencies] pycparser = "*" +[[package]] +name = "charm-api" +version = "0.1.0" +description = "" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [] +develop = false + +[package.source] +type = "git" +url = "https://github.com/canonical/charm-api" +reference = "HEAD" +resolved_reference = "c76857d71b309bd28ba5330b0880d179184b0f87" + +[[package]] +name = "charm-json" +version = "0.1.0" +description = "" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [] +develop = false + +[package.dependencies] +charm-api = {git = "https://github.com/canonical/charm-api"} + +[package.source] +type = "git" +url = "https://github.com/canonical/charm-json" +reference = "HEAD" +resolved_reference = "dd96b4c1b4a44255dfd20c0ca7d4f4a28e01ead7" + +[[package]] +name = "charm-refresh" +version = "0.1.0" +description = "" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [] +develop = false + +[package.dependencies] +charm-api = {git = "https://github.com/canonical/charm-api"} +charm-json = {git = "https://github.com/canonical/charm-json"} +httpx = ">=0.28.1" +lightkube = ">=0.15.4" +ops = ">=2.9.0" +packaging = ">=24.1" +pyyaml = ">=6.0.2" +tomli = ">=2.0.1" + +[package.source] +type = "git" +url = "https://github.com/canonical/charm-refresh" +reference = "draft" +resolved_reference = "7f52a6887904342f91b512c8bfcbcac970f6e21f" + [[package]] name = "charset-normalizer" version = "3.3.2" @@ -596,7 +657,7 @@ version = "1.2.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" -groups = ["charm-libs", "integration", "unit"] +groups = ["main", "charm-libs", "integration", "unit"] markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, @@ -684,7 +745,7 @@ version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" -groups = ["charm-libs"] +groups = ["main", "charm-libs"] files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, @@ -696,7 +757,7 @@ version = "1.0.9" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" -groups = ["charm-libs"] +groups = ["main", "charm-libs"] files = [ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, @@ -718,7 +779,7 @@ version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" -groups = ["charm-libs"] +groups = ["main", "charm-libs"] files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -1018,7 +1079,7 @@ version = "0.17.1" description = "Lightweight kubernetes client library" optional = false python-versions = "*" -groups = ["charm-libs"] +groups = ["main", "charm-libs"] files = [ {file = "lightkube-0.17.1-py3-none-any.whl", hash = "sha256:3d046c2c46191b3745471763710ef4ed2df4259a7405f798b577df2ae390358a"}, {file = "lightkube-0.17.1.tar.gz", hash = "sha256:e0d6b71476a4fa7cbda7080da1f0943e43c7e747212db9f2ec7d87415bf8d23e"}, @@ -1038,7 +1099,7 @@ version = "1.32.0.8" description = "Models and Resources for lightkube module" optional = false python-versions = "*" -groups = ["charm-libs"] +groups = ["main", "charm-libs"] files = [ {file = "lightkube-models-1.32.0.8.tar.gz", hash = "sha256:97f6c2ab554a23a69554dd56ffbd94173fb416af6490c3a21b1e0b8e13a2bafe"}, {file = "lightkube_models-1.32.0.8-py3-none-any.whl", hash = "sha256:73786dac63085521f4c88aa69d86bfdc76a67da997c1770e5bdcef8482e4b2a0"}, @@ -1360,7 +1421,7 @@ version = "24.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["integration", "unit"] +groups = ["main", "integration", "unit"] files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, @@ -1852,63 +1913,65 @@ files = [ [[package]] name = "pyyaml" -version = "6.0.1" +version = "6.0.2" description = "YAML parser and emitter for Python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" groups = ["main", "charm-libs", "integration", "unit"] files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] [[package]] @@ -2137,7 +2200,7 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" -groups = ["charm-libs"] +groups = ["main", "charm-libs"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -2185,7 +2248,7 @@ version = "2.0.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.7" -groups = ["integration", "unit"] +groups = ["main", "integration", "unit"] files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, @@ -2226,11 +2289,12 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" -groups = ["charm-libs", "integration"] +groups = ["main", "charm-libs", "integration"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +markers = {main = "python_version < \"3.11\""} [[package]] name = "typing-inspect" @@ -2490,4 +2554,4 @@ test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.funct [metadata] lock-version = "2.1" python-versions = "^3.8.6" -content-hash = "b1b2343a3fd7fde050c0b57530703da1686dbeb169c934f7b3fd7697a1d0c531" +content-hash = "614c92ef3194008d18f429a0066a06c66856852e13614528e09f133aec8f59b6" diff --git a/pyproject.toml b/pyproject.toml index a35a5c93..05c09e8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ tenacity = "^9.0.0" poetry-core = "^1.9.1" jinja2 = "^3.1.4" requests = "^2.32.3" +charm-refresh = {git = "https://github.com/canonical/charm-refresh", rev = "draft"} [tool.poetry.group.charm-libs.dependencies] # data_platform_libs/v0/data_interfaces.py diff --git a/refresh_versions.toml b/refresh_versions.toml new file mode 100644 index 00000000..c805ed08 --- /dev/null +++ b/refresh_versions.toml @@ -0,0 +1,14 @@ +charm_major = 1 +workload = "8.0.41" + +# autogenerated +charm = "8.0/1.0.0" + +[snap] +name = "charmed-mysql" + +[snap.revisions] +# amd64 +x86_64 = "139" +# arm64 +aarch64 = "138" diff --git a/src/abstract_charm.py b/src/abstract_charm.py index d7d40011..153d90e6 100644 --- a/src/abstract_charm.py +++ b/src/abstract_charm.py @@ -4,26 +4,54 @@ """MySQL Router charm""" import abc +import dataclasses import logging import typing +import charm_refresh import ops import container import lifecycle import logrotate -import machine_upgrade import relations.cos import relations.database_provides import relations.database_requires import relations.tls import server_exceptions -import upgrade import workload logger = logging.getLogger(__name__) +@dataclasses.dataclass(eq=False) +class RouterRefresh(charm_refresh.CharmSpecificCommon): + """MySQL Router refresh callbacks & configuration""" + + @staticmethod + def run_pre_refresh_checks_after_1_unit_refreshed() -> None: + pass + + @classmethod + def is_compatible( + cls, + *, + old_charm_version: charm_refresh.CharmVersion, + new_charm_version: charm_refresh.CharmVersion, + old_workload_version: str, + new_workload_version: str, + ) -> bool: + if not super().is_compatible( + old_charm_version=old_charm_version, + new_charm_version=new_charm_version, + old_workload_version=old_workload_version, + new_workload_version=new_workload_version, + ): + return False + # TODO: check workload version—prevent downgrade? + return True + + class MySQLRouterCharm(ops.CharmBase, abc.ABC): """MySQL Router charm""" @@ -32,6 +60,8 @@ class MySQLRouterCharm(ops.CharmBase, abc.ABC): _READ_WRITE_X_PORT = 6448 _READ_ONLY_X_PORT = 6449 + refresh: charm_refresh.Common + def __init__(self, *args) -> None: super().__init__(*args) # Instantiate before registering other event observers @@ -40,33 +70,19 @@ def __init__(self, *args) -> None: ) self._workload_type = workload.Workload - self._authenticated_workload_type = workload.AuthenticatedWorkload + self._running_workload_type = workload.RunningWorkload self._database_requires = relations.database_requires.RelationEndpoint(self) self._database_provides = relations.database_provides.RelationEndpoint(self) self._cos_relation = relations.cos.COSRelation(self, self._container) self._ha_cluster = None - self.framework.observe(self.on.update_status, self.reconcile) - self.framework.observe( - self.on[upgrade.PEER_RELATION_ENDPOINT_NAME].relation_changed, self.reconcile - ) - self.framework.observe( - self.on[upgrade.RESUME_ACTION_NAME].action, self._on_resume_upgrade_action - ) - # (For Kubernetes) Reset partition after scale down - self.framework.observe( - self.on[upgrade.PEER_RELATION_ENDPOINT_NAME].relation_departed, self.reconcile - ) - # Handle upgrade & set status on first start if no relations active - self.framework.observe(self.on.start, self.reconcile) - # Update app status - self.framework.observe(self.on.leader_elected, self.reconcile) - # Set versions in upgrade peer relation app databag - self.framework.observe( - self.on[upgrade.PEER_RELATION_ENDPOINT_NAME].relation_created, - self._upgrade_relation_created, - ) self.tls = relations.tls.RelationEndpoint(self) + # Observe all events (except custom events) + for bound_event in self.on.events().values(): + if bound_event.event_type == ops.CollectStatusEvent: + continue + self.framework.observe(bound_event, self.reconcile) + @property @abc.abstractmethod def _subordinate_relation_endpoint_names(self) -> typing.Optional[typing.Iterable[str]]: @@ -80,11 +96,6 @@ def _subordinate_relation_endpoint_names(self) -> typing.Optional[typing.Iterabl def _container(self) -> container.Container: """Workload container (snap or rock)""" - @property - @abc.abstractmethod - def _upgrade(self) -> typing.Optional[upgrade.Upgrade]: - pass - @property @abc.abstractmethod def _logrotate(self) -> logrotate.LogRotate: @@ -162,10 +173,17 @@ def _cos_exporter_config(self, event) -> typing.Optional[relations.cos.ExporterC if cos_relation_exists: return self._cos_relation.exporter_user_config - def get_workload(self, *, event): - """MySQL Router workload""" - if connection_info := self._database_requires.get_connection_info(event=event): - return self._authenticated_workload_type( + def get_workload(self, *, event, refresh: charm_refresh.Common): + """MySQL Router workload + + Pass `refresh` if `self.refresh` is not set + """ + if refresh is None: + refresh = self.refresh + if refresh.workload_allowed_to_start and ( + connection_info := self._database_requires.get_connection_info(event=event) + ): + return self._running_workload_type( container_=self._container, logrotate_=self._logrotate, connection_info=connection_info, @@ -196,13 +214,10 @@ def _prioritize_statuses(statuses: typing.List[ops.StatusBase]) -> ops.StatusBas return status return ops.ActiveStatus() - def _determine_app_status(self, *, event) -> ops.StatusBase: + def _determine_app_status(self, *, event, refresh: charm_refresh.Common) -> ops.StatusBase: """Report app status.""" - if self._upgrade and (upgrade_status := self._upgrade.app_status): - # Upgrade status should take priority over relation status—even if the status level is - # normally lower priority. - # (Relations should not be modified during upgrade.) - return upgrade_status + if refresh.app_status_higher_priority: + return refresh.app_status_higher_priority statuses = [] if self._status: statuses.append(self._status) @@ -211,25 +226,32 @@ def _determine_app_status(self, *, event) -> ops.StatusBase: statuses.append(status) return self._prioritize_statuses(statuses) - def _determine_unit_status(self, *, event) -> ops.StatusBase: + def _determine_unit_status(self, *, event, refresh: charm_refresh.Common) -> ops.StatusBase: """Report unit status.""" + if refresh.unit_status_higher_priority: + return refresh.unit_status_higher_priority statuses = [] - workload_status = self.get_workload(event=event).status - if self._upgrade: - statuses.append(self._upgrade.get_unit_juju_status(workload_status=workload_status)) + workload_ = self.get_workload(event=event, refresh=refresh) + if status := workload_.status: + statuses.append(status) # only in machine charms if self._ha_cluster: - statuses.append(self._ha_cluster.get_unit_juju_status()) - statuses.append(workload_status) + if status := self._ha_cluster.get_unit_juju_status(): + statuses.append(status) + refresh_lower_priority = refresh.unit_status_lower_priority( + workload_is_running=isinstance(workload_, workload.RunningWorkload) + ) + if (not statuses or statuses == [ops.WaitingStatus()]) and refresh_lower_priority: + return refresh_lower_priority return self._prioritize_statuses(statuses) - def set_status(self, *, event, app=True, unit=True) -> None: + def set_status(self, *, event, refresh: charm_refresh.Common, app=True, unit=True) -> None: """Set charm status.""" if app and self._unit_lifecycle.authorized_leader: - self.app.status = self._determine_app_status(event=event) + self.app.status = self._determine_app_status(event=event, refresh=refresh) logger.debug(f"Set app status to {self.app.status}") if unit: - self.unit.status = self._determine_unit_status(event=event) + self.unit.status = self._determine_unit_status(event=event, refresh=refresh) logger.debug(f"Set unit status to {self.unit.status}") @abc.abstractmethod @@ -261,67 +283,30 @@ def _update_endpoints(self) -> None: # Handlers # ======================= - def _upgrade_relation_created(self, _) -> None: - if self._unit_lifecycle.authorized_leader: - # `self._upgrade.is_compatible` should return `True` during first charm - # installation/setup - self._upgrade.set_versions_in_app_databag() - - def reconcile(self, event=None) -> None: # noqa: C901 + def reconcile(self, event=None, *, refresh: charm_refresh.Common = None) -> None: # noqa: C901 """Handle most events.""" - if not self._upgrade: - logger.debug("Peer relation not available") - return - if not self._upgrade.versions_set: - logger.debug("Peer relation not ready") - return - workload_ = self.get_workload(event=event) - if self._unit_lifecycle.authorized_leader and not self._upgrade.in_progress: - # Run before checking `self._upgrade.is_compatible` in case incompatible upgrade was - # forced & completed on all units. - # Side effect: on machines, if charm was upgraded to a charm with the same snap - # revision, compatibility checks will be skipped. - # (The only real use case for this would be upgrading the charm code to an incompatible - # version without upgrading the snap. In that situation, the upgrade may appear - # successful and the user will not be notified of the charm incompatibility. This case - # is much less likely than the forced incompatible upgrade & the impact is not as bad - # as the impact if we did not handle the forced incompatible upgrade case.) - self._upgrade.set_versions_in_app_databag() - if self._upgrade.unit_state is upgrade.UnitState.RESTARTING: # Kubernetes only - if not self._upgrade.is_compatible: - logger.info( - "Upgrade incompatible. If you accept potential *data loss* and *downtime*, you can continue with `resume-upgrade force=true`" - ) - self.unit.status = ops.BlockedStatus( - "Upgrade incompatible. Rollback to previous revision with `juju refresh`" - ) - self.set_status(event=event, unit=False) - return - elif isinstance(self._upgrade, machine_upgrade.Upgrade): # Machines only - if not self._upgrade.is_compatible: - self.set_status(event=event) - return - if self._upgrade.unit_state is upgrade.UnitState.OUTDATED: - if self._upgrade.authorized: - self._upgrade.upgrade_unit( - event=event, - workload_=workload_, - tls=self._tls_certificate_saved, - exporter_config=self._cos_exporter_config(event), - ) - else: - self.set_status(event=event) - logger.debug("Waiting to upgrade") - return + if refresh is None: + refresh = self.refresh + workload_ = self.get_workload(event=event, refresh=refresh) logger.debug( "State of reconcile " f"{self._unit_lifecycle.authorized_leader=}, " - f"{isinstance(workload_, workload.AuthenticatedWorkload)=}, " + f"{isinstance(workload_, workload.RunningWorkload)=}, " f"{workload_.container_ready=}, " + f"{refresh.workload_allowed_to_start=}, " f"{self._database_requires.is_relation_breaking(event)=}, " - f"{self._upgrade.in_progress=}, " + f"{self._database_requires.does_relation_exist()=}, " + f"{refresh.in_progress=}, " f"{self._cos_relation.is_relation_breaking(event)=}" ) + if isinstance(refresh, charm_refresh.Machines): + workload_.install( + unit=self.unit, + model_uuid=self.model.uuid, + snap_revision=refresh.pinned_snap_revision, + refresh=refresh, + ) + self.unit.set_workload_version(workload_.version) # only in machine charms if self._ha_cluster: @@ -330,14 +315,14 @@ def reconcile(self, event=None) -> None: # noqa: C901 try: if self._unit_lifecycle.authorized_leader: if self._database_requires.is_relation_breaking(event): - if self._upgrade.in_progress: + if refresh.in_progress: logger.warning( "Modifying relations during an upgrade is not supported. The charm may be in a broken, unrecoverable state. Re-deploy the charm" ) self._database_provides.delete_all_databags() elif ( - not self._upgrade.in_progress - and isinstance(workload_, workload.AuthenticatedWorkload) + not refresh.in_progress + and isinstance(workload_, workload.RunningWorkload) and workload_.container_ready ): self._reconcile_service() @@ -361,33 +346,25 @@ def reconcile(self, event=None) -> None: # noqa: C901 certificate=self._tls_certificate, certificate_authority=self._tls_certificate_authority, ) - if not self._upgrade.in_progress and isinstance( - workload_, workload.AuthenticatedWorkload - ): + if not refresh.in_progress and isinstance(workload_, workload.RunningWorkload): self._reconcile_ports(event=event) - # Empty waiting status means we're waiting for database requires relation before - # starting workload - if not workload_.status or workload_.status == ops.WaitingStatus(): - self._upgrade.unit_state = upgrade.UnitState.HEALTHY - if self._unit_lifecycle.authorized_leader: - self._upgrade.reconcile_partition() - self.set_status(event=event) + logger.debug(f"{workload_.status=}") + if not workload_.status: + refresh.next_unit_allowed_to_refresh = True + elif refresh.workload_allowed_to_start and workload_.status == ops.WaitingStatus(): + # During scale up, this code should not be reached before the first + # relation-created event is received on this unit since otherwise + # `charm_refresh.PeerRelationNotReady` would be raised + if self._database_requires.does_relation_exist(): + # Waiting for relation-changed event before starting workload + pass + else: + # Waiting for database requires relation; refresh can continue + refresh.next_unit_allowed_to_refresh = True + self.set_status(event=event, refresh=refresh) except server_exceptions.Error as e: # If not for `unit=False`, another `server_exceptions.Error` could be thrown here - self.set_status(event=event, unit=False) + self.set_status(event=event, refresh=refresh, unit=False) self.unit.status = e.status logger.debug(f"Set unit status to {self.unit.status}") - - def _on_resume_upgrade_action(self, event: ops.ActionEvent) -> None: - if not self._unit_lifecycle.authorized_leader: - message = f"Must run action on leader unit. (e.g. `juju run {self.app.name}/leader {upgrade.RESUME_ACTION_NAME}`)" - logger.debug(f"Resume upgrade event failed: {message}") - event.fail(message) - return - if not self._upgrade or not self._upgrade.in_progress: - message = "No upgrade in progress" - logger.debug(f"Resume upgrade event failed: {message}") - event.fail(message) - return - self._upgrade.reconcile_partition(action_event=event) diff --git a/src/charm.py b/src/charm.py index 4b3aecb3..b94cff28 100755 --- a/src/charm.py +++ b/src/charm.py @@ -13,33 +13,55 @@ if is_wrong_architecture() and __name__ == "__main__": ops.main.main(WrongArchitectureWarningCharm) +import dataclasses import logging import socket import typing +import charm_refresh import tenacity from charms.tempo_coordinator_k8s.v0.charm_tracing import trace_charm import abstract_charm import logrotate import machine_logrotate -import machine_upgrade import machine_workload import relations.database_providers_wrapper import relations.hacluster import snap -import upgrade import workload logger = logging.getLogger(__name__) +logging.getLogger("httpx").setLevel(logging.WARNING) +logging.getLogger("httpcore").setLevel(logging.WARNING) + + +@dataclasses.dataclass(eq=False) +class _MachinesRouterRefresh(abstract_charm.RouterRefresh, charm_refresh.CharmSpecificMachines): + _charm: abstract_charm.MySQLRouterCharm + + def refresh_snap( + self, *, snap_name: str, snap_revision: str, refresh: charm_refresh.Machines + ) -> None: + # TODO: issue on relation-broken event since event not passed? mitigated by regular event handler? + self._charm.get_workload(event=None, refresh=refresh).refresh( + event=None, + unit=self._charm.unit, + model_uuid=self._charm.model.uuid, + snap_revision=snap_revision, + refresh=refresh, + tls=self._charm._tls_certificate_saved, + exporter_config=self._charm._cos_exporter_config(event=None), + ) + # Set `refresh.next_unit_allowed_to_refresh = True` + self._charm.reconcile(refresh=refresh) @trace_charm( tracing_endpoint="tracing_endpoint", extra_types=( logrotate.LogRotate, - machine_upgrade.Upgrade, - machine_workload.AuthenticatedMachineWorkload, + machine_workload.RunningMachineWorkload, relations.cos.COSRelation, relations.database_providers_wrapper.RelationEndpoint, relations.database_requires.RelationEndpoint, @@ -57,15 +79,27 @@ def __init__(self, *args) -> None: self._database_provides = relations.database_providers_wrapper.RelationEndpoint( self, self._database_provides ) - self._authenticated_workload_type = machine_workload.AuthenticatedMachineWorkload + self._running_workload_type = machine_workload.RunningMachineWorkload self._ha_cluster = relations.hacluster.HACluster(self) - self.framework.observe(self.on.install, self._on_install) - self.framework.observe(self.on.remove, self._on_remove) - self.framework.observe(self.on.upgrade_charm, self._on_upgrade_charm) - self.framework.observe( - self.on[machine_upgrade.FORCE_ACTION_NAME].action, self._on_force_upgrade_action - ) - self.framework.observe(self.on.config_changed, self.reconcile) + try: + self.refresh = charm_refresh.Machines( + _MachinesRouterRefresh( + workload_name="Router", + refresh_user_docs_url="https://charmhub.io/mysql-router/docs/h-upgrade", + _charm=self, + ) + ) + except charm_refresh.UnitTearingDown: + # MySQL server charm will clean up users & router metadata when the MySQL Router app or + # unit(s) tear down + self.unit.status = ops.MaintenanceStatus("Tearing down") + snap.uninstall() + exit() + except charm_refresh.PeerRelationNotReady: + self.unit.status = ops.MaintenanceStatus("Waiting for peer relation") + if self.unit.is_leader(): + self.app.status = ops.MaintenanceStatus("Waiting for peer relation") + exit() @property def _subordinate_relation_endpoint_names(self) -> typing.Optional[typing.Iterable[str]]: @@ -78,13 +112,6 @@ def _subordinate_relation_endpoint_names(self) -> typing.Optional[typing.Iterabl def _container(self) -> snap.Snap: return snap.Snap(unit_name=self.unit.name) - @property - def _upgrade(self) -> typing.Optional[machine_upgrade.Upgrade]: - try: - return machine_upgrade.Upgrade(self) - except upgrade.PeerRelationNotReady: - pass - @property def _status(self) -> ops.StatusBase: pass @@ -180,56 +207,6 @@ def wait_until_mysql_router_ready(self, *, event) -> None: else: logger.debug("MySQL Router is ready") - # ======================= - # Handlers - # ======================= - - def _on_install(self, _) -> None: - snap.install(unit=self.unit, model_uuid=self.model.uuid) - self.unit.set_workload_version(self.get_workload(event=None).version) - - def _on_remove(self, _) -> None: - snap.uninstall() - - def _on_upgrade_charm(self, _) -> None: - if self._unit_lifecycle.authorized_leader: - if not self._upgrade.in_progress: - logger.info("Charm upgraded. MySQL Router version unchanged") - self._upgrade.upgrade_resumed = False - # Only call `reconcile` on leader unit to avoid race conditions with `upgrade_resumed` - self.reconcile() - - def _on_resume_upgrade_action(self, event: ops.ActionEvent) -> None: - super()._on_resume_upgrade_action(event) - # If next to upgrade, upgrade leader unit - self.reconcile() - - def _on_force_upgrade_action(self, event: ops.ActionEvent) -> None: - if not self._upgrade or not self._upgrade.in_progress: - message = "No upgrade in progress" - logger.debug(f"Force upgrade event failed: {message}") - event.fail(message) - return - if not self._upgrade.upgrade_resumed: - message = f"Run `juju run {self.app.name}/leader resume-upgrade` before trying to force upgrade" - logger.debug(f"Force upgrade event failed: {message}") - event.fail(message) - return - if self._upgrade.unit_state is not upgrade.UnitState.OUTDATED: - message = "Unit already upgraded" - logger.debug(f"Force upgrade event failed: {message}") - event.fail(message) - return - - logger.warning("Forcing upgrade") - event.log(f"Forcefully upgrading {self.unit.name}") - self._upgrade.upgrade_unit( - event=event, workload_=self.get_workload(event=None), tls=self._tls_certificate_saved - ) - self.reconcile() - event.set_results({"result": f"Forcefully upgraded {self.unit.name}"}) - logger.warning("Forced upgrade") - if __name__ == "__main__": ops.main.main(MachineSubordinateRouterCharm) diff --git a/src/container.py b/src/container.py index 8ddd61d1..e72864e6 100644 --- a/src/container.py +++ b/src/container.py @@ -8,6 +8,7 @@ import subprocess import typing +import charm_refresh import ops if typing.TYPE_CHECKING: @@ -61,6 +62,13 @@ def __init__( super().__init__(returncode=returncode, cmd=cmd, output=output, stderr=stderr) +class RefreshFailed(Exception): + """Snap failed to refresh. Previous snap revision is still installed + + Only applies to machine charm + """ + + class Container(abc.ABC): """Workload container (snap or rock)""" @@ -163,11 +171,33 @@ def update_mysql_router_exporter_service( "`key`, `certificate` and `certificate_authority` required when tls=True" ) + @staticmethod + @abc.abstractmethod + def install( + *, unit: ops.Unit, model_uuid: str, snap_revision: str, refresh: charm_refresh.Machines + ) -> None: + """Ensure snap is installed by this charm + + Only applies to machine charm + + If snap is not installed, install it + If snap is installed, check that it was installed by this charm & raise an exception otherwise + + Automatically retries if snap installation fails + """ + + @staticmethod @abc.abstractmethod - def upgrade(self, unit: ops.Unit) -> None: - """Upgrade container version + def refresh( + *, unit: ops.Unit, model_uuid: str, snap_revision: str, refresh: charm_refresh.Machines + ) -> None: + """Refresh snap Only applies to machine charm + + If snap refresh fails and previous revision is still installed, raises `RefreshFailed` + + Does not automatically retry if snap installation fails """ @abc.abstractmethod diff --git a/src/machine_upgrade.py b/src/machine_upgrade.py deleted file mode 100644 index a016358f..00000000 --- a/src/machine_upgrade.py +++ /dev/null @@ -1,198 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -"""In-place upgrades on machines - -Derived from specification: DA058 - In-Place Upgrades - Kubernetes v2 -(https://docs.google.com/document/d/1tLjknwHudjcHs42nzPVBNkHs98XxAOT2BXGGpP7NyEU/) -""" - -import json -import logging -import time -import typing - -import ops - -import snap -import upgrade -import workload - -if typing.TYPE_CHECKING: - import relations.cos - -logger = logging.getLogger(__name__) - -FORCE_ACTION_NAME = "force-upgrade" - - -class Upgrade(upgrade.Upgrade): - """In-place upgrades on machines""" - - @property - def unit_state(self) -> typing.Optional[upgrade.UnitState]: - if ( - self._unit_workload_container_version is not None - and self._unit_workload_container_version != self._app_workload_container_version - ): - logger.debug("Unit upgrade state: outdated") - return upgrade.UnitState.OUTDATED - return super().unit_state - - @unit_state.setter - def unit_state(self, value: upgrade.UnitState) -> None: - if value is upgrade.UnitState.HEALTHY: - # Set snap revision on first install - self._unit_workload_container_version = snap.revision - self._unit_workload_version = self._current_versions["workload"] - logger.debug( - f'Saved {snap.revision=} and {self._current_versions["workload"]=} in unit databag while setting state healthy' - ) - # Super call - upgrade.Upgrade.unit_state.fset(self, value) - - def _get_unit_healthy_status( - self, *, workload_status: typing.Optional[ops.StatusBase] - ) -> ops.StatusBase: - if self._unit_workload_container_version == self._app_workload_container_version: - if isinstance(workload_status, ops.WaitingStatus): - return ops.WaitingStatus( - f'Router {self._unit_workload_version}; Snap rev {self._unit_workload_container_version}; Charmed operator {self._current_versions["charm"]}' - ) - return ops.ActiveStatus( - f'Router {self._unit_workload_version} running; Snap rev {self._unit_workload_container_version}; Charmed operator {self._current_versions["charm"]}' - ) - if isinstance(workload_status, ops.WaitingStatus): - return ops.WaitingStatus( - f'Router {self._unit_workload_version}; Snap rev {self._unit_workload_container_version} (outdated); Charmed operator {self._current_versions["charm"]}' - ) - return ops.ActiveStatus( - f'Router {self._unit_workload_version} running; Snap rev {self._unit_workload_container_version} (outdated); Charmed operator {self._current_versions["charm"]}' - ) - - @property - def app_status(self) -> typing.Optional[ops.StatusBase]: - if not self.in_progress: - return - if not self.is_compatible: - logger.info( - "Upgrade incompatible. If you accept potential *data loss* and *downtime*, you can continue by running `force-upgrade` action on each remaining unit" - ) - return ops.BlockedStatus( - "Upgrade incompatible. Rollback to previous revision with `juju refresh`" - ) - return super().app_status - - @property - def _unit_workload_container_versions(self) -> typing.Dict[str, str]: - """{Unit name: installed snap revision}""" - versions = {} - for unit in self._sorted_units: - if version := (self._peer_relation.data[unit].get("snap_revision")): - versions[unit.name] = version - return versions - - @property - def _unit_workload_container_version(self) -> typing.Optional[str]: - """Installed snap revision for this unit""" - return self._unit_databag.get("snap_revision") - - @_unit_workload_container_version.setter - def _unit_workload_container_version(self, value: str): - self._unit_databag["snap_revision"] = value - - @property - def _app_workload_container_version(self) -> str: - """Snap revision for current charm code""" - return snap.revision - - @property - def _unit_workload_version(self) -> typing.Optional[str]: - """Installed MySQL Router version for this unit""" - return self._unit_databag.get("workload_version") - - @_unit_workload_version.setter - def _unit_workload_version(self, value: str): - self._unit_databag["workload_version"] = value - - def reconcile_partition(self, *, action_event: ops.ActionEvent = None) -> None: - """Handle Juju action to confirm first upgraded unit is healthy and resume upgrade.""" - if action_event: - unit = self._sorted_units[0] # First unit to upgrade - state = self._peer_relation.data[unit].get("state") - if state: - state = upgrade.UnitState(state) - outdated = ( - self._unit_workload_container_versions.get(unit.name) - != self._app_workload_container_version - ) - unhealthy = state is not upgrade.UnitState.HEALTHY - if outdated or unhealthy: - if outdated: - message = "Highest number unit has not upgraded yet. Upgrade will not resume." - else: - message = "Highest number unit is unhealthy. Upgrade will not resume." - logger.debug(f"Resume upgrade event failed: {message}") - action_event.fail(message) - return - self.upgrade_resumed = True - message = "Upgrade resumed." - action_event.set_results({"result": message}) - logger.debug(f"Resume upgrade event succeeded: {message}") - - @property - def upgrade_resumed(self) -> bool: - """Whether user has resumed upgrade with Juju action - - Reset to `False` after each `juju refresh` - """ - return json.loads(self._app_databag.get("upgrade-resumed", "false")) - - @upgrade_resumed.setter - def upgrade_resumed(self, value: bool): - # Trigger peer relation_changed event even if value does not change - # (Needed when leader sets value to False during `ops.UpgradeCharmEvent`) - self._app_databag["-unused-timestamp-upgrade-resume-last-updated"] = str(time.time()) - - self._app_databag["upgrade-resumed"] = json.dumps(value) - logger.debug(f"Set upgrade-resumed to {value=}") - - @property - def authorized(self) -> bool: - assert self._unit_workload_container_version != self._app_workload_container_version - for index, unit in enumerate(self._sorted_units): - if unit.name == self._unit.name: - # Higher number units have already upgraded - if index == 1: - # User confirmation needed to resume upgrade (i.e. upgrade second unit) - logger.debug(f"Second unit authorized to upgrade if {self.upgrade_resumed=}") - return self.upgrade_resumed - return True - state = self._peer_relation.data[unit].get("state") - if state: - state = upgrade.UnitState(state) - if ( - self._unit_workload_container_versions.get(unit.name) - != self._app_workload_container_version - or state is not upgrade.UnitState.HEALTHY - ): - # Waiting for higher number units to upgrade - return False - return False - - def upgrade_unit( - self, - *, - event, - workload_: workload.Workload, - tls: bool, - exporter_config: "relations.cos.ExporterConfig", - ) -> None: - logger.debug(f"Upgrading {self.authorized=}") - self.unit_state = upgrade.UnitState.UPGRADING - workload_.upgrade(event=event, unit=self._unit, tls=tls, exporter_config=exporter_config) - self._unit_workload_container_version = snap.revision - self._unit_workload_version = self._current_versions["workload"] - logger.debug( - f'Saved {snap.revision=} and {self._current_versions["workload"]=} in unit databag after upgrade' - ) diff --git a/src/machine_workload.py b/src/machine_workload.py index ed713e66..59ea234c 100644 --- a/src/machine_workload.py +++ b/src/machine_workload.py @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) -class AuthenticatedMachineWorkload(workload.AuthenticatedWorkload): +class RunningMachineWorkload(workload.RunningWorkload): """Workload with connection to MySQL cluster and with Unix sockets enabled""" # TODO python3.10 min version: Use `list` instead of `typing.List` diff --git a/src/relations/cos.py b/src/relations/cos.py index d25dfcc9..14f83c68 100644 --- a/src/relations/cos.py +++ b/src/relations/cos.py @@ -7,13 +7,13 @@ import typing from dataclasses import dataclass +import charm_refresh import ops from charms.grafana_agent.v0.cos_agent import COSAgentProvider, charm_tracing_config import container import relations.secrets import utils -from snap import _SNAP_NAME if typing.TYPE_CHECKING: import abstract_charm @@ -53,21 +53,12 @@ def __init__(self, charm_: "abstract_charm.MySQLRouterCharm", container_: contai "port": self._EXPORTER_PORT, } ], - log_slots=[f"{_SNAP_NAME}:logs"], + log_slots=[f"{charm_refresh.snap_name()}:logs"], tracing_protocols=[self._TRACING_PROTOCOL], ) self._charm = charm_ self._container = container_ - charm_.framework.observe( - charm_.on[self._NAME].relation_created, - charm_.reconcile, - ) - charm_.framework.observe( - charm_.on[self._NAME].relation_broken, - charm_.reconcile, - ) - self._secrets = relations.secrets.RelationSecrets( charm_, self._PEER_RELATION_NAME, diff --git a/src/relations/database_provides.py b/src/relations/database_provides.py index b1876c94..b00507aa 100644 --- a/src/relations/database_provides.py +++ b/src/relations/database_provides.py @@ -214,10 +214,7 @@ class RelationEndpoint: def __init__(self, charm_: "abstract_charm.MySQLRouterCharm") -> None: self._interface = data_interfaces.DatabaseProvides(charm_, relation_name=self._NAME) - - charm_.framework.observe(charm_.on[self._NAME].relation_created, charm_.reconcile) charm_.framework.observe(self._interface.on.database_requested, charm_.reconcile) - charm_.framework.observe(charm_.on[self._NAME].relation_broken, charm_.reconcile) @property # TODO python3.10 min version: Use `list` instead of `typing.List` diff --git a/src/relations/database_requires.py b/src/relations/database_requires.py index bdb23da3..1e1b3379 100644 --- a/src/relations/database_requires.py +++ b/src/relations/database_requires.py @@ -6,6 +6,7 @@ import logging import typing +import charm_ as charm import charms.data_platform_libs.v0.data_interfaces as data_interfaces import ops @@ -109,10 +110,8 @@ def __init__(self, charm_: "abstract_charm.MySQLRouterCharm") -> None: database_name="mysql_innodb_cluster_metadata", extra_user_roles="mysqlrouter", ) - charm_.framework.observe(charm_.on[self._NAME].relation_created, charm_.reconcile) charm_.framework.observe(self._interface.on.database_created, charm_.reconcile) charm_.framework.observe(self._interface.on.endpoints_changed, charm_.reconcile) - charm_.framework.observe(charm_.on[self._NAME].relation_broken, charm_.reconcile) def get_connection_info(self, *, event) -> typing.Optional[CompleteConnectionInformation]: """Information for connection to MySQL cluster""" @@ -137,3 +136,11 @@ def get_status(self, event) -> typing.Optional[ops.StatusBase]: CompleteConnectionInformation(interface=self._interface, event=event) except (_MissingRelation, remote_databag.IncompleteDatabag) as exception: return exception.status + + def does_relation_exist(self) -> bool: + """Whether a relation exists + + From testing: during scale up, this should return `True` as soon as this unit receives the + first relation-created event on any endpoint + """ + return charm.Endpoint(self._NAME).relation is not None diff --git a/src/relations/deprecated_shared_db_database_provides.py b/src/relations/deprecated_shared_db_database_provides.py index 02d596c9..bf4f86ea 100644 --- a/src/relations/deprecated_shared_db_database_provides.py +++ b/src/relations/deprecated_shared_db_database_provides.py @@ -205,8 +205,6 @@ def __init__(self, charm_: "abstract_charm.MySQLRouterCharm") -> None: logger.warning( "'mysql-shared' relation interface is DEPRECATED and will be removed in a future release. Use 'mysql_client' interface instead." ) - charm_.framework.observe(charm_.on[self._NAME].relation_changed, charm_.reconcile) - charm_.framework.observe(charm_.on[self._NAME].relation_broken, charm_.reconcile) self._charm = charm_ self.framework.observe( self._charm.on[self._CREDENTIALS_PEER_RELATION_ENDPOINT_NAME].relation_changed, diff --git a/src/relations/hacluster.py b/src/relations/hacluster.py index cf1f3906..c8f31a49 100644 --- a/src/relations/hacluster.py +++ b/src/relations/hacluster.py @@ -23,10 +23,6 @@ def __init__(self, charm: ops.CharmBase): self.charm = charm - self.framework.observe( - self.charm.on[HACLUSTER_RELATION_NAME].relation_changed, self.charm.reconcile - ) - @property def relation(self) -> Optional[ops.Relation]: """Returns the relations in this model, or None if hacluster is not initialised.""" diff --git a/src/snap.py b/src/snap.py index 98b27944..656739fb 100644 --- a/src/snap.py +++ b/src/snap.py @@ -3,14 +3,13 @@ """Workload snap container & installer""" -import enum import logging import pathlib -import platform import shutil import subprocess import typing +import charm_refresh import charms.operator_libs_linux.v2.snap as snap_lib import ops import tenacity @@ -22,73 +21,41 @@ logger = logging.getLogger(__name__) -_SNAP_NAME = "charmed-mysql" -REVISIONS: typing.Dict[str, str] = { - # Keep in sync with `workload_version` file - "x86_64": "139", - "aarch64": "138", -} -revision = REVISIONS[platform.machine()] -_snap = snap_lib.SnapCache()[_SNAP_NAME] +_snap_name = charm_refresh.snap_name() +_snap = snap_lib.SnapCache()[_snap_name] +_installed_by_unit = pathlib.Path( + "/var/snap", _snap_name, "common", "installed_by_mysql_router_charm_unit" +) _UNIX_USERNAME = "snap_daemon" -class _RefreshVerb(str, enum.Enum): - INSTALL = "install" - UPGRADE = "upgrade" +def _unique_unit_name(*, unit: ops.Unit, model_uuid: str): + return f"{model_uuid}_{unit.name}" -def _refresh(*, unit: ops.Unit, verb: _RefreshVerb) -> None: - # TODO python3.10 min version: use `removesuffix` instead of `rstrip` - logger.debug(f'{verb.capitalize().rstrip("e")}ing {_SNAP_NAME=}, {revision=}') - unit.status = ops.MaintenanceStatus(f'{verb.capitalize().rstrip("e")}ing snap') +def _raise_if_snap_installed_not_by_this_charm(*, unit: ops.Unit, model_uuid: str): + """Raise exception if snap was not installed by this charm - def _set_retry_status(_) -> None: - message = f"Snap {verb} failed. Retrying..." - unit.status = ops.MaintenanceStatus(message) - logger.debug(message) - - for attempt in tenacity.Retrying( - stop=tenacity.stop_after_delay(60 * 5), - wait=tenacity.wait_exponential(multiplier=10), - retry=tenacity.retry_if_exception_type(snap_lib.SnapError), - after=_set_retry_status, - reraise=True, - ): - with attempt: - _snap.ensure(state=snap_lib.SnapState.Present, revision=revision) - _snap.hold() - logger.debug(f'{verb.capitalize().rstrip("e")}ed {_SNAP_NAME=}, {revision=}') - - -def install(*, unit: ops.Unit, model_uuid: str): - """Install snap.""" - installed_by_unit = pathlib.Path( - "/var/snap", _SNAP_NAME, "common", "installed_by_mysql_router_charm_unit" - ) - unique_unit_name = f"{model_uuid}_{unit.name}" - # This charm can override/use an existing snap installation only if the snap was previously - # installed by this charm. - # Otherwise, the snap could be in use by another charm (e.g. MySQL Server charm, a different - # MySQL Router charm). - if _snap.present and not ( - installed_by_unit.exists() and installed_by_unit.read_text() == unique_unit_name + Assumes snap is installed + """ + if not ( + _installed_by_unit.exists() + and _installed_by_unit.read_text() == _unique_unit_name(unit=unit, model_uuid=model_uuid) ): + # The snap could be in use by another charm (e.g. MySQL Server charm, a different MySQL + # Router charm). logger.debug( - f"{installed_by_unit.exists() and installed_by_unit.read_text()=} {unique_unit_name=}" + f"{_installed_by_unit.exists() and _installed_by_unit.read_text()=} {_unique_unit_name(unit=unit, model_uuid=model_uuid)=}" ) - logger.error(f"{_SNAP_NAME} snap already installed on machine. Installation aborted") - raise Exception(f"Multiple {_SNAP_NAME} snap installs not supported on one machine") - _refresh(unit=unit, verb=_RefreshVerb.INSTALL) - installed_by_unit.write_text(unique_unit_name) - logger.debug(f"Wrote {unique_unit_name=} to {installed_by_unit.name=}") + logger.error(f"{_snap_name} snap already installed on machine. Installation aborted") + raise Exception(f"Multiple {_snap_name} snap installs not supported on one machine") def uninstall(): - """Uninstall snap.""" - logger.debug(f"Uninstalling {_SNAP_NAME=}") + """Uninstall snap if installed""" + logger.debug(f"Ensuring {_snap_name=} is uninstalled") _snap.ensure(state=snap_lib.SnapState.Absent) - logger.debug(f"Uninstalled {_SNAP_NAME=}") + logger.debug(f"Ensured {_snap_name=} is uninstalled") class _Path(pathlib.PosixPath, container.Path): @@ -100,13 +67,13 @@ def __new__(cls, *args, **kwargs): if str(path).startswith("/etc/mysqlrouter") or str(path).startswith( "/var/lib/mysqlrouter" ): - parent = f"/var/snap/{_SNAP_NAME}/current" + parent = f"/var/snap/{_snap_name}/current" elif str(path).startswith("/run/mysqlrouter") or str(path).startswith( "/var/log/mysqlrouter" ): - parent = f"/var/snap/{_SNAP_NAME}/common" + parent = f"/var/snap/{_snap_name}/common" elif str(path).startswith("/tmp"): - parent = f"/tmp/snap-private-tmp/snap.{_SNAP_NAME}" + parent = f"/tmp/snap-private-tmp/snap.{_snap_name}" else: parent = None if parent: @@ -162,9 +129,9 @@ class Snap(container.Container): def __init__(self, *, unit_name: str) -> None: super().__init__( - mysql_router_command=f"{_SNAP_NAME}.mysqlrouter", - mysql_shell_command=f"{_SNAP_NAME}.mysqlsh", - mysql_router_password_command=f"{_SNAP_NAME}.mysqlrouter-passwd", + mysql_router_command=f"{_snap_name}.mysqlrouter", + mysql_shell_command=f"{_snap_name}.mysqlsh", + mysql_router_password_command=f"{_snap_name}.mysqlrouter-passwd", unit_name=unit_name, ) @@ -247,9 +214,85 @@ def update_mysql_router_exporter_service( _snap.unset("mysqlrouter.tls-cert-path") _snap.unset("mysqlrouter.tls-key-path") - def upgrade(self, unit: ops.Unit) -> None: - """Upgrade snap.""" - _refresh(unit=unit, verb=_RefreshVerb.UPGRADE) + @staticmethod + def install( + *, unit: ops.Unit, model_uuid: str, snap_revision: str, refresh: charm_refresh.Machines + ) -> None: + """Ensure snap is installed by this charm + + If snap is not installed, install it + If snap is installed, check that it was installed by this charm & raise an exception otherwise + + Automatically retries if snap installation fails + """ + unique_unit_name = f"{model_uuid}_{unit.name}" + if _snap.present: + _raise_if_snap_installed_not_by_this_charm(unit=unit, model_uuid=model_uuid) + return + # Install snap + logger.info(f"Installing snap revision {repr(snap_revision)}") + unit.status = ops.MaintenanceStatus("Installing snap") + + def _set_retry_status(_) -> None: + message = "Snap install failed. Retrying..." + unit.status = ops.MaintenanceStatus(message) + logger.debug(message) + + for attempt in tenacity.Retrying( + stop=tenacity.stop_after_delay(60 * 5), + wait=tenacity.wait_exponential(multiplier=10), + retry=tenacity.retry_if_exception_type((snap_lib.SnapError, snap_lib.SnapAPIError)), + after=_set_retry_status, + reraise=True, + ): + with attempt: + _snap.ensure(state=snap_lib.SnapState.Present, revision=snap_revision) + refresh.update_snap_revision() + _snap.hold() + _installed_by_unit.write_text(unique_unit_name) + logger.debug(f"Wrote {unique_unit_name=} to {_installed_by_unit.name=}") + logger.info(f"Installed snap revision {repr(snap_revision)}") + + @classmethod + def refresh( + cls, + *, + unit: ops.Unit, + model_uuid: str, + snap_revision: str, + refresh: charm_refresh.Machines, + ) -> None: + """Refresh snap + + If snap refresh fails and previous revision is still installed, raises `RefreshFailed` + + Does not automatically retry if snap installation fails + """ + if not _snap.present: + cls.install( + unit=unit, model_uuid=model_uuid, snap_revision=snap_revision, refresh=refresh + ) + return + _raise_if_snap_installed_not_by_this_charm(unit=unit, model_uuid=model_uuid) + + revision_before_refresh = _snap.revision + if revision_before_refresh == snap_revision: + raise ValueError(f"Cannot refresh snap; {snap_revision=} is already installed") + + logger.info(f"Refreshing snap to revision {repr(snap_revision)}") + unit.status = ops.MaintenanceStatus("Refreshing snap") + try: + _snap.ensure(state=snap_lib.SnapState.Present, revision=snap_revision) + except (snap_lib.SnapError, snap_lib.SnapAPIError): + logger.exception("Snap refresh failed") + if _snap.revision == revision_before_refresh: + raise container.RefreshFailed + else: + refresh.update_snap_revision() + raise + else: + refresh.update_snap_revision() + logger.info(f"Refreshed snap to revision {repr(snap_revision)}") # TODO python3.10 min version: Use `list` instead of `typing.List` def _run_command( diff --git a/src/upgrade.py b/src/upgrade.py deleted file mode 100644 index 0535c89f..00000000 --- a/src/upgrade.py +++ /dev/null @@ -1,230 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -"""In-place upgrades - -Based off specification: DA058 - In-Place Upgrades - Kubernetes v2 -(https://docs.google.com/document/d/1tLjknwHudjcHs42nzPVBNkHs98XxAOT2BXGGpP7NyEU/) -""" - -import abc -import copy -import enum -import json -import logging -import pathlib -import typing - -import ops -import poetry.core.constraints.version as poetry_version - -import workload - -logger = logging.getLogger(__name__) - -PEER_RELATION_ENDPOINT_NAME = "upgrade-version-a" -RESUME_ACTION_NAME = "resume-upgrade" - - -def unit_number(unit_: ops.Unit) -> int: - """Get unit number""" - return int(unit_.name.split("/")[-1]) - - -class PeerRelationNotReady(Exception): - """Upgrade peer relation not available (to this unit)""" - - -class UnitState(str, enum.Enum): - """Unit upgrade state""" - - HEALTHY = "healthy" - RESTARTING = "restarting" # Kubernetes only - UPGRADING = "upgrading" # Machines only - OUTDATED = "outdated" # Machines only - - -class Upgrade(abc.ABC): - """In-place upgrades""" - - def __init__(self, charm_: ops.CharmBase) -> None: - relations = charm_.model.relations[PEER_RELATION_ENDPOINT_NAME] - if not relations: - raise PeerRelationNotReady - assert len(relations) == 1 - self._peer_relation = relations[0] - self._unit: ops.Unit = charm_.unit - self._unit_databag = self._peer_relation.data[self._unit] - self._app_databag = self._peer_relation.data[charm_.app] - self._app_name = charm_.app.name - self._current_versions = {} # For this unit - for version, file_name in { - "charm": "charm_version", - "workload": "workload_version", - }.items(): - self._current_versions[version] = pathlib.Path(file_name).read_text().strip() - - @property - def unit_state(self) -> typing.Optional[UnitState]: - """Unit upgrade state""" - if state := self._unit_databag.get("state"): - return UnitState(state) - - @unit_state.setter - def unit_state(self, value: UnitState) -> None: - self._unit_databag["state"] = value.value - - @property - def is_compatible(self) -> bool: - """Whether upgrade is supported from previous versions""" - assert self.versions_set - previous_version_strs: typing.Dict[str, str] = json.loads(self._app_databag["versions"]) - # TODO charm versioning: remove `.split("+")` (which removes git hash before comparing) - previous_version_strs["charm"] = previous_version_strs["charm"].split("+")[0] - previous_versions: typing.Dict[str, poetry_version.Version] = { - key: poetry_version.Version.parse(value) - for key, value in previous_version_strs.items() - } - current_version_strs = copy.copy(self._current_versions) - current_version_strs["charm"] = current_version_strs["charm"].split("+")[0] - current_versions = { - key: poetry_version.Version.parse(value) for key, value in current_version_strs.items() - } - try: - if ( - previous_versions["charm"] > current_versions["charm"] - or previous_versions["charm"].major != current_versions["charm"].major - ): - logger.debug( - f'{previous_versions["charm"]=} incompatible with {current_versions["charm"]=}' - ) - return False - if ( - previous_versions["workload"] > current_versions["workload"] - or previous_versions["workload"].major != current_versions["workload"].major - or previous_versions["workload"].minor != current_versions["workload"].minor - ): - logger.debug( - f'{previous_versions["workload"]=} incompatible with {current_versions["workload"]=}' - ) - return False - logger.debug( - f"Versions before upgrade compatible with versions after upgrade {previous_version_strs=} {self._current_versions=}" - ) - return True - except KeyError as exception: - logger.debug(f"Version missing from {previous_versions=}", exc_info=exception) - return False - - @property - def in_progress(self) -> bool: - logger.debug( - f"{self._app_workload_container_version=} {self._unit_workload_container_versions=}" - ) - return any( - version != self._app_workload_container_version - for version in self._unit_workload_container_versions.values() - ) - - @property - def _sorted_units(self) -> typing.List[ops.Unit]: - """Units sorted from highest to lowest unit number""" - return sorted((self._unit, *self._peer_relation.units), key=unit_number, reverse=True) - - @abc.abstractmethod - def _get_unit_healthy_status( - self, *, workload_status: typing.Optional[ops.StatusBase] - ) -> ops.StatusBase: - """Status shown during upgrade if unit is healthy""" - - def get_unit_juju_status( - self, *, workload_status: typing.Optional[ops.StatusBase] - ) -> typing.Optional[ops.StatusBase]: - if self.in_progress: - return self._get_unit_healthy_status(workload_status=workload_status) - - @property - def app_status(self) -> typing.Optional[ops.StatusBase]: - if not self.in_progress: - return - if not self.upgrade_resumed: - # User confirmation needed to resume upgrade (i.e. upgrade second unit) - # Statuses over 120 characters are truncated in `juju status` as of juju 3.1.6 and - # 2.9.45 - return ops.BlockedStatus( - f"Upgrading. Verify highest unit is healthy & run `{RESUME_ACTION_NAME}` action. To rollback, `juju refresh` to last revision" - ) - return ops.MaintenanceStatus( - "Upgrading. To rollback, `juju refresh` to the previous revision" - ) - - @property - def versions_set(self) -> bool: - """Whether versions have been saved in app databag - - Should only be `False` during first charm install - - If a user upgrades from a charm that does not set versions, this charm will get stuck. - """ - return self._app_databag.get("versions") is not None - - def set_versions_in_app_databag(self) -> None: - """Save current versions in app databag - - Used after next upgrade to check compatibility (i.e. whether that upgrade should be - allowed) - """ - assert not self.in_progress - logger.debug(f"Setting {self._current_versions=} in upgrade peer relation app databag") - self._app_databag["versions"] = json.dumps(self._current_versions) - logger.debug(f"Set {self._current_versions=} in upgrade peer relation app databag") - - @property - @abc.abstractmethod - def upgrade_resumed(self) -> bool: - """Whether user has resumed upgrade with Juju action""" - - @property - @abc.abstractmethod - def _unit_workload_container_versions(self) -> typing.Dict[str, str]: - """{Unit name: unique identifier for unit's workload container version} - - If and only if this version changes, the workload will restart (during upgrade or - rollback). - - On Kubernetes, the workload & charm are upgraded together - On machines, the charm is upgraded before the workload - - This identifier should be comparable to `_app_workload_container_version` to determine if - the unit & app are the same workload container version. - """ - - @property - @abc.abstractmethod - def _app_workload_container_version(self) -> str: - """Unique identifier for the app's workload container version - - This should match the workload version in the current Juju app charm version. - - This identifier should be comparable to `_unit_workload_container_versions` to determine if - the app & unit are the same workload container version. - """ - - @abc.abstractmethod - def reconcile_partition(self, *, action_event: ops.ActionEvent = None) -> None: - """If ready, allow next unit to upgrade.""" - - @property - @abc.abstractmethod - def authorized(self) -> bool: - """Whether this unit is authorized to upgrade - - Only applies to machine charm - """ - - @abc.abstractmethod - def upgrade_unit(self, *, event, workload_: workload.Workload, tls: bool) -> None: - """Upgrade this unit. - - Only applies to machine charm - """ diff --git a/src/workload.py b/src/workload.py index 2643641e..29f6a044 100644 --- a/src/workload.py +++ b/src/workload.py @@ -11,6 +11,7 @@ import string import typing +import charm_refresh import ops import requests import tenacity @@ -76,16 +77,45 @@ def version(self) -> str: return component return "" - def upgrade( - self, *, event, unit: ops.Unit, tls: bool, exporter_config: "relations.cos.ExporterConfig" + def install( + self, + *, + unit: ops.Unit, + model_uuid: str, + snap_revision: str, + refresh: charm_refresh.Machines, + ) -> None: + """Ensure snap is installed by this charm + + Only applies to machine charm + + If snap is not installed, install it + If snap is installed, check that it was installed by this charm & raise an exception otherwise + + Automatically retries if snap installation fails + """ + self._container.install( + unit=unit, model_uuid=model_uuid, snap_revision=snap_revision, refresh=refresh + ) + + def refresh( + self, + *, + event, + unit: ops.Unit, + model_uuid: str, + snap_revision: str, + refresh: charm_refresh.Machines, + tls: bool, + exporter_config: "relations.cos.ExporterConfig", ) -> None: - """Upgrade MySQL Router. + """Refresh MySQL Router Only applies to machine charm """ - logger.debug("Upgrading MySQL Router") - self._container.upgrade(unit=unit) - logger.debug("Upgraded MySQL Router") + self._container.refresh( + unit=unit, model_uuid=model_uuid, snap_revision=snap_revision, refresh=refresh + ) @property def _tls_config_file_data(self) -> str: @@ -186,7 +216,7 @@ def status(self) -> typing.Optional[ops.StatusBase]: return ops.WaitingStatus() -class AuthenticatedWorkload(Workload): +class RunningWorkload(Workload): """Workload with connection to MySQL cluster""" def __init__( @@ -420,8 +450,16 @@ def status(self) -> typing.Optional[ops.StatusBase]: "Router was manually removed from MySQL ClusterSet. Remove & re-deploy unit" ) - def upgrade( - self, *, event, unit: ops.Unit, tls: bool, exporter_config: "relations.cos.ExporterConfig" + def refresh( + self, + *, + event, + unit: ops.Unit, + model_uuid: str, + snap_revision: str, + refresh: charm_refresh.Machines, + tls: bool, + exporter_config: "relations.cos.ExporterConfig", ) -> None: enabled = self._container.mysql_router_service_enabled exporter_enabled = self._container.mysql_router_exporter_service_enabled @@ -430,12 +468,27 @@ def upgrade( if enabled: logger.debug("Disabling MySQL Router service before upgrade") self._disable_router() - super().upgrade(event=event, unit=unit, tls=tls, exporter_config=exporter_config) - if enabled: - logger.debug("Re-enabling MySQL Router service after upgrade") - self._enable_router(event=event, tls=tls, unit_name=unit.name) - if exporter_enabled: - self._enable_exporter(tls=tls, exporter_config=exporter_config) + try: + super().refresh( + event=event, + unit=unit, + model_uuid=model_uuid, + snap_revision=snap_revision, + refresh=refresh, + tls=tls, + exporter_config=exporter_config, + ) + except container.RefreshFailed: + message = "Re-enabling MySQL Router service after failed snap refresh" + raise + else: + message = "Re-enabling MySQL Router service after refresh" + finally: + if enabled: + logger.debug(message) + self._enable_router(event=event, tls=tls, unit_name=unit.name) + if exporter_enabled: + self._enable_exporter(tls=tls, exporter_config=exporter_config) def _wait_until_http_server_authenticates(self) -> None: """Wait until active connection with router HTTP server using monitoring credentials.""" diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index c0b402d0..8835afec 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -35,7 +35,7 @@ def patch(monkeypatch): "charm.MachineSubordinateRouterCharm.wait_until_mysql_router_ready", lambda *args, **kwargs: None, ) - monkeypatch.setattr("workload.AuthenticatedWorkload._router_username", "") + monkeypatch.setattr("workload.RunningWorkload._router_username", "") monkeypatch.setattr("mysql_shell.Shell._run_code", lambda *args, **kwargs: None) monkeypatch.setattr( "mysql_shell.Shell.get_mysql_router_user_for_unit", lambda *args, **kwargs: None diff --git a/tests/unit/test_workload.py b/tests/unit/test_workload.py index 64ef369b..13296d6e 100644 --- a/tests/unit/test_workload.py +++ b/tests/unit/test_workload.py @@ -185,4 +185,4 @@ ], ) def test_parse_username_from_config(config_file_text, username): - assert workload.AuthenticatedWorkload._parse_username_from_config(config_file_text) == username + assert workload.RunningWorkload._parse_username_from_config(config_file_text) == username diff --git a/workload_version b/workload_version deleted file mode 100644 index d5c63883..00000000 --- a/workload_version +++ /dev/null @@ -1 +0,0 @@ -8.0.41 From db9b897ad8cc8dac810253cf0250b30a07800a88 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Fri, 4 Apr 2025 11:30:23 +0200 Subject: [PATCH 02/51] Simplify reconcile Avoid passing refresh object since we know reconcile will be called later (no need to explicitly call it & have it run twice) --- src/abstract_charm.py | 60 ++++++++++++++++++++++--------------------- src/charm.py | 5 ++-- 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/src/abstract_charm.py b/src/abstract_charm.py index 153d90e6..7cd58bfc 100644 --- a/src/abstract_charm.py +++ b/src/abstract_charm.py @@ -173,7 +173,7 @@ def _cos_exporter_config(self, event) -> typing.Optional[relations.cos.ExporterC if cos_relation_exists: return self._cos_relation.exporter_user_config - def get_workload(self, *, event, refresh: charm_refresh.Common): + def get_workload(self, *, event, refresh: charm_refresh.Common = None): """MySQL Router workload Pass `refresh` if `self.refresh` is not set @@ -214,10 +214,10 @@ def _prioritize_statuses(statuses: typing.List[ops.StatusBase]) -> ops.StatusBas return status return ops.ActiveStatus() - def _determine_app_status(self, *, event, refresh: charm_refresh.Common) -> ops.StatusBase: + def _determine_app_status(self, *, event) -> ops.StatusBase: """Report app status.""" - if refresh.app_status_higher_priority: - return refresh.app_status_higher_priority + if self.refresh.app_status_higher_priority: + return self.refresh.app_status_higher_priority statuses = [] if self._status: statuses.append(self._status) @@ -226,32 +226,32 @@ def _determine_app_status(self, *, event, refresh: charm_refresh.Common) -> ops. statuses.append(status) return self._prioritize_statuses(statuses) - def _determine_unit_status(self, *, event, refresh: charm_refresh.Common) -> ops.StatusBase: + def _determine_unit_status(self, *, event) -> ops.StatusBase: """Report unit status.""" - if refresh.unit_status_higher_priority: - return refresh.unit_status_higher_priority + if self.refresh.unit_status_higher_priority: + return self.refresh.unit_status_higher_priority statuses = [] - workload_ = self.get_workload(event=event, refresh=refresh) + workload_ = self.get_workload(event=event) if status := workload_.status: statuses.append(status) # only in machine charms if self._ha_cluster: if status := self._ha_cluster.get_unit_juju_status(): statuses.append(status) - refresh_lower_priority = refresh.unit_status_lower_priority( + refresh_lower_priority = self.refresh.unit_status_lower_priority( workload_is_running=isinstance(workload_, workload.RunningWorkload) ) if (not statuses or statuses == [ops.WaitingStatus()]) and refresh_lower_priority: return refresh_lower_priority return self._prioritize_statuses(statuses) - def set_status(self, *, event, refresh: charm_refresh.Common, app=True, unit=True) -> None: + def set_status(self, *, event, app=True, unit=True) -> None: """Set charm status.""" if app and self._unit_lifecycle.authorized_leader: - self.app.status = self._determine_app_status(event=event, refresh=refresh) + self.app.status = self._determine_app_status(event=event) logger.debug(f"Set app status to {self.app.status}") if unit: - self.unit.status = self._determine_unit_status(event=event, refresh=refresh) + self.unit.status = self._determine_unit_status(event=event) logger.debug(f"Set unit status to {self.unit.status}") @abc.abstractmethod @@ -283,28 +283,26 @@ def _update_endpoints(self) -> None: # Handlers # ======================= - def reconcile(self, event=None, *, refresh: charm_refresh.Common = None) -> None: # noqa: C901 + def reconcile(self, event=None) -> None: # noqa: C901 """Handle most events.""" - if refresh is None: - refresh = self.refresh - workload_ = self.get_workload(event=event, refresh=refresh) + workload_ = self.get_workload(event=event) logger.debug( "State of reconcile " f"{self._unit_lifecycle.authorized_leader=}, " f"{isinstance(workload_, workload.RunningWorkload)=}, " f"{workload_.container_ready=}, " - f"{refresh.workload_allowed_to_start=}, " + f"{self.refresh.workload_allowed_to_start=}, " f"{self._database_requires.is_relation_breaking(event)=}, " f"{self._database_requires.does_relation_exist()=}, " - f"{refresh.in_progress=}, " + f"{self.refresh.in_progress=}, " f"{self._cos_relation.is_relation_breaking(event)=}" ) - if isinstance(refresh, charm_refresh.Machines): + if isinstance(self.refresh, charm_refresh.Machines): workload_.install( unit=self.unit, model_uuid=self.model.uuid, - snap_revision=refresh.pinned_snap_revision, - refresh=refresh, + snap_revision=self.refresh.pinned_snap_revision, + refresh=self.refresh, ) self.unit.set_workload_version(workload_.version) @@ -315,13 +313,13 @@ def reconcile(self, event=None, *, refresh: charm_refresh.Common = None) -> None try: if self._unit_lifecycle.authorized_leader: if self._database_requires.is_relation_breaking(event): - if refresh.in_progress: + if self.refresh.in_progress: logger.warning( "Modifying relations during an upgrade is not supported. The charm may be in a broken, unrecoverable state. Re-deploy the charm" ) self._database_provides.delete_all_databags() elif ( - not refresh.in_progress + not self.refresh.in_progress and isinstance(workload_, workload.RunningWorkload) and workload_.container_ready ): @@ -346,13 +344,17 @@ def reconcile(self, event=None, *, refresh: charm_refresh.Common = None) -> None certificate=self._tls_certificate, certificate_authority=self._tls_certificate_authority, ) - if not refresh.in_progress and isinstance(workload_, workload.RunningWorkload): + if not self.refresh.in_progress and isinstance( + workload_, workload.RunningWorkload + ): self._reconcile_ports(event=event) logger.debug(f"{workload_.status=}") if not workload_.status: - refresh.next_unit_allowed_to_refresh = True - elif refresh.workload_allowed_to_start and workload_.status == ops.WaitingStatus(): + self.refresh.next_unit_allowed_to_refresh = True + elif ( + self.refresh.workload_allowed_to_start and workload_.status == ops.WaitingStatus() + ): # During scale up, this code should not be reached before the first # relation-created event is received on this unit since otherwise # `charm_refresh.PeerRelationNotReady` would be raised @@ -361,10 +363,10 @@ def reconcile(self, event=None, *, refresh: charm_refresh.Common = None) -> None pass else: # Waiting for database requires relation; refresh can continue - refresh.next_unit_allowed_to_refresh = True - self.set_status(event=event, refresh=refresh) + self.refresh.next_unit_allowed_to_refresh = True + self.set_status(event=event) except server_exceptions.Error as e: # If not for `unit=False`, another `server_exceptions.Error` could be thrown here - self.set_status(event=event, refresh=refresh, unit=False) + self.set_status(event=event, unit=False) self.unit.status = e.status logger.debug(f"Set unit status to {self.unit.status}") diff --git a/src/charm.py b/src/charm.py index b94cff28..c0af10c9 100755 --- a/src/charm.py +++ b/src/charm.py @@ -53,8 +53,9 @@ def refresh_snap( tls=self._charm._tls_certificate_saved, exporter_config=self._charm._cos_exporter_config(event=None), ) - # Set `refresh.next_unit_allowed_to_refresh = True` - self._charm.reconcile(refresh=refresh) + # `reconcile()` will run on every event, which will set + # `refresh.next_unit_allowed_to_refresh = True` + # (This method will run in the charm's __init__, before `reconcile()` is called by ops) @trace_charm( From 85b1a1b65162f239c95121d8d447a236e3f5eb0e Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Mon, 7 Apr 2025 16:36:59 +0200 Subject: [PATCH 03/51] Add `self._reconcile_allowed` instead of exiting --- src/abstract_charm.py | 9 +++++++++ src/charm.py | 6 ++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/abstract_charm.py b/src/abstract_charm.py index 7cd58bfc..d3e81e80 100644 --- a/src/abstract_charm.py +++ b/src/abstract_charm.py @@ -61,6 +61,12 @@ class MySQLRouterCharm(ops.CharmBase, abc.ABC): _READ_ONLY_X_PORT = 6449 refresh: charm_refresh.Common + # Whether `reconcile` method is allowed to run + # `False` if `charm_refresh.UnitTearingDown` or `charm_refresh.PeerRelationNotReady` raised + # Most of the charm code should not run if either of those exceptions is raised + # However, some charm libs (i.e. data-platform-libs) will break if they do not receive every + # event they expect (e.g. relation-created) + _reconcile_allowed: bool def __init__(self, *args) -> None: super().__init__(*args) @@ -285,6 +291,9 @@ def _update_endpoints(self) -> None: def reconcile(self, event=None) -> None: # noqa: C901 """Handle most events.""" + if not self._reconcile_allowed: + logger.debug("Reconcile not allowed") + return workload_ = self.get_workload(event=event) logger.debug( "State of reconcile " diff --git a/src/charm.py b/src/charm.py index c0af10c9..45160ff2 100755 --- a/src/charm.py +++ b/src/charm.py @@ -95,12 +95,14 @@ def __init__(self, *args) -> None: # unit(s) tear down self.unit.status = ops.MaintenanceStatus("Tearing down") snap.uninstall() - exit() + self._reconcile_allowed = False except charm_refresh.PeerRelationNotReady: self.unit.status = ops.MaintenanceStatus("Waiting for peer relation") if self.unit.is_leader(): self.app.status = ops.MaintenanceStatus("Waiting for peer relation") - exit() + self._reconcile_allowed = False + else: + self._reconcile_allowed = True @property def _subordinate_relation_endpoint_names(self) -> typing.Optional[typing.Iterable[str]]: From 4461680ff014f1090a7e7f45c419b3590ed4bfd6 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Thu, 10 Apr 2025 10:26:22 +0200 Subject: [PATCH 04/51] Sync with K8s --- src/abstract_charm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abstract_charm.py b/src/abstract_charm.py index d3e81e80..c3a61b76 100644 --- a/src/abstract_charm.py +++ b/src/abstract_charm.py @@ -25,7 +25,7 @@ @dataclasses.dataclass(eq=False) -class RouterRefresh(charm_refresh.CharmSpecificCommon): +class RouterRefresh(charm_refresh.CharmSpecificCommon, abc.ABC): """MySQL Router refresh callbacks & configuration""" @staticmethod From 461d501e1b41d86f8f18d7f79562dc0a6748e44f Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Fri, 11 Apr 2025 15:22:52 +0200 Subject: [PATCH 05/51] Remove workload version set --- poetry.lock | 6 +++--- src/abstract_charm.py | 1 - src/workload.py | 9 --------- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/poetry.lock b/poetry.lock index 1b6fc1ec..bfc21cce 100644 --- a/poetry.lock +++ b/poetry.lock @@ -305,7 +305,7 @@ develop = false type = "git" url = "https://github.com/canonical/charm-api" reference = "HEAD" -resolved_reference = "c76857d71b309bd28ba5330b0880d179184b0f87" +resolved_reference = "d298e56e355679b8f2095a149fdb83b77601c3f2" [[package]] name = "charm-json" @@ -329,7 +329,7 @@ resolved_reference = "dd96b4c1b4a44255dfd20c0ca7d4f4a28e01ead7" [[package]] name = "charm-refresh" version = "0.1.0" -description = "" +description = "In-place rolling refreshes of stateful charmed applications " optional = false python-versions = ">=3.8" groups = ["main"] @@ -350,7 +350,7 @@ tomli = ">=2.0.1" type = "git" url = "https://github.com/canonical/charm-refresh" reference = "draft" -resolved_reference = "7f52a6887904342f91b512c8bfcbcac970f6e21f" +resolved_reference = "b4e8db416505d5340983b7c7155c81e9406aca61" [[package]] name = "charset-normalizer" diff --git a/src/abstract_charm.py b/src/abstract_charm.py index c3a61b76..d0492d5b 100644 --- a/src/abstract_charm.py +++ b/src/abstract_charm.py @@ -313,7 +313,6 @@ def reconcile(self, event=None) -> None: # noqa: C901 snap_revision=self.refresh.pinned_snap_revision, refresh=self.refresh, ) - self.unit.set_workload_version(workload_.version) # only in machine charms if self._ha_cluster: diff --git a/src/workload.py b/src/workload.py index 29f6a044..d2bc01c9 100644 --- a/src/workload.py +++ b/src/workload.py @@ -68,15 +68,6 @@ def container_ready(self) -> bool: """ return self._container.ready - @property - def version(self) -> str: - """MySQL Router version""" - version = self._container.run_mysql_router(["--version"]) - for component in version.split(): - if component.startswith("8"): - return component - return "" - def install( self, *, From 26868e234171985d7c5157c2ce542f24e0e0e6c5 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Tue, 15 Apr 2025 11:10:06 +0200 Subject: [PATCH 06/51] Include logger name in logs --- src/charm.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/charm.py b/src/charm.py index 45160ff2..d3858242 100755 --- a/src/charm.py +++ b/src/charm.py @@ -19,6 +19,7 @@ import typing import charm_refresh +import ops.log import tenacity from charms.tempo_coordinator_k8s.v0.charm_tracing import trace_charm @@ -76,6 +77,12 @@ class MachineSubordinateRouterCharm(abstract_charm.MySQLRouterCharm): def __init__(self, *args) -> None: super().__init__(*args) + # Show logger name (module name) in logs + root_logger = logging.getLogger() + for handler in root_logger.handlers: + if isinstance(handler, ops.log.JujuLogHandler): + handler.setFormatter(logging.Formatter("{name}:{message}", style="{")) + # DEPRECATED shared-db: Enable legacy "mysql-shared" interface self._database_provides = relations.database_providers_wrapper.RelationEndpoint( self, self._database_provides From 1ee0c39f59214398d5e54a0bd562fb95567550d3 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Thu, 17 Apr 2025 15:04:06 +0200 Subject: [PATCH 07/51] Update charm-refresh --- poetry.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index bfc21cce..ca4986ad 100644 --- a/poetry.lock +++ b/poetry.lock @@ -305,7 +305,7 @@ develop = false type = "git" url = "https://github.com/canonical/charm-api" reference = "HEAD" -resolved_reference = "d298e56e355679b8f2095a149fdb83b77601c3f2" +resolved_reference = "ce5da6d930931feb578779627113d81436f3ae69" [[package]] name = "charm-json" @@ -350,7 +350,7 @@ tomli = ">=2.0.1" type = "git" url = "https://github.com/canonical/charm-refresh" reference = "draft" -resolved_reference = "b4e8db416505d5340983b7c7155c81e9406aca61" +resolved_reference = "240c8c1a9cc225ac2cf038da30be3022700bc950" [[package]] name = "charset-normalizer" From 20c5b453300d4688d994ae1c9962b1ca25d4190b Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Thu, 24 Apr 2025 13:27:27 +0200 Subject: [PATCH 08/51] Update charm_refresh --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index ca4986ad..bcfaed13 100644 --- a/poetry.lock +++ b/poetry.lock @@ -350,7 +350,7 @@ tomli = ">=2.0.1" type = "git" url = "https://github.com/canonical/charm-refresh" reference = "draft" -resolved_reference = "240c8c1a9cc225ac2cf038da30be3022700bc950" +resolved_reference = "de8c58161758c4344ddb18aa6d54e11c0c6d3aa4" [[package]] name = "charset-normalizer" From 4ea0037a2715fe3abc8201012dbd8077885dd2d3 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Wed, 7 May 2025 11:06:53 +0200 Subject: [PATCH 09/51] Use JSON schema --- actions.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/actions.yaml b/actions.yaml index 85144490..61a57e4f 100644 --- a/actions.yaml +++ b/actions.yaml @@ -3,6 +3,7 @@ pre-refresh-check: description: Check if charm is ready to refresh + additionalProperties: false force-refresh-start: description: | Potential of data loss and downtime @@ -32,7 +33,7 @@ force-refresh-start: Potential of data loss and downtime during and after refresh If `false`, allow refresh to MySQL Router container version that has not been validated to work with the charm revision - required: [] + additionalProperties: false resume-refresh: description: | Refresh next unit(s) (after you have manually verified that refreshed units are healthy) @@ -54,7 +55,7 @@ resume-refresh: Warning: if first unit to refresh is unhealthy, consider running `force-refresh-start` action on that unit instead of using this parameter. If first unit to refresh is unhealthy because compatibility checks, pre-refresh checks, or workload container checks are failing, this parameter is more destructive than the `force-refresh-start` action. - required: [] + additionalProperties: false set-tls-private-key: description: From a605bd457ca5ccc5ceac31b7b48a7f2d91eecb82 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Wed, 7 May 2025 11:08:31 +0200 Subject: [PATCH 10/51] Use released charm-refresh --- charmcraft.yaml | 2 +- poetry.lock | 50 +++++++++++++++++++------------------------------ pyproject.toml | 2 +- src/charm.py | 4 +--- 4 files changed, 22 insertions(+), 36 deletions(-) diff --git a/charmcraft.yaml b/charmcraft.yaml index b0de9dd7..ea240da2 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -51,7 +51,7 @@ parts: source: . after: - poetry-deps - poetry-export-extra-args: ['--only', 'main,charm-libs', '--without-hashes'] # TODO: re-enable hashes + poetry-export-extra-args: ['--only', 'main,charm-libs'] build-packages: - libffi-dev # Needed to build Python dependencies with Rust from source - libssl-dev # Needed to build Python dependencies with Rust from source diff --git a/poetry.lock b/poetry.lock index bcfaed13..e38c404f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -293,52 +293,46 @@ pycparser = "*" [[package]] name = "charm-api" -version = "0.1.0" +version = "0.1.1" description = "" optional = false python-versions = ">=3.8" groups = ["main"] -files = [] -develop = false - -[package.source] -type = "git" -url = "https://github.com/canonical/charm-api" -reference = "HEAD" -resolved_reference = "ce5da6d930931feb578779627113d81436f3ae69" +files = [ + {file = "charm_api-0.1.1-py3-none-any.whl", hash = "sha256:2fb02cee06a198e025a9a25f9e2b80bdecac62e07f0e0b9dca217031328184aa"}, + {file = "charm_api-0.1.1.tar.gz", hash = "sha256:8e55e6ae4b484548a6c48eb83d68af39a77910c1aff9596b13ddc7c1e319fabc"}, +] [[package]] name = "charm-json" -version = "0.1.0" +version = "0.1.1" description = "" optional = false python-versions = ">=3.8" groups = ["main"] -files = [] -develop = false +files = [ + {file = "charm_json-0.1.1-py3-none-any.whl", hash = "sha256:a3fac62d45821d1a8c14058632e21333ec4e2cd41d0d00d6a73d00fc9a656eef"}, + {file = "charm_json-0.1.1.tar.gz", hash = "sha256:cb2eb24f6135d226ad04b0a17288ca2e027160d8af288083ef701bf4b137154e"}, +] [package.dependencies] -charm-api = {git = "https://github.com/canonical/charm-api"} - -[package.source] -type = "git" -url = "https://github.com/canonical/charm-json" -reference = "HEAD" -resolved_reference = "dd96b4c1b4a44255dfd20c0ca7d4f4a28e01ead7" +charm-api = ">=0.1.1" [[package]] name = "charm-refresh" -version = "0.1.0" +version = "3.0.0.1" description = "In-place rolling refreshes of stateful charmed applications " optional = false python-versions = ">=3.8" groups = ["main"] -files = [] -develop = false +files = [ + {file = "charm_refresh-3.0.0.1-py3-none-any.whl", hash = "sha256:d67f37c4f66bf94e2c778ef53935c0cc6fb48b6635f58b910130760ba71e7c8d"}, + {file = "charm_refresh-3.0.0.1.tar.gz", hash = "sha256:066bd8a8c96bbfa519f286c8f1ed4d4f80c83b22b8d1929301cba80420bd0079"}, +] [package.dependencies] -charm-api = {git = "https://github.com/canonical/charm-api"} -charm-json = {git = "https://github.com/canonical/charm-json"} +charm-api = ">=0.1.1" +charm-json = ">=0.1.1" httpx = ">=0.28.1" lightkube = ">=0.15.4" ops = ">=2.9.0" @@ -346,12 +340,6 @@ packaging = ">=24.1" pyyaml = ">=6.0.2" tomli = ">=2.0.1" -[package.source] -type = "git" -url = "https://github.com/canonical/charm-refresh" -reference = "draft" -resolved_reference = "de8c58161758c4344ddb18aa6d54e11c0c6d3aa4" - [[package]] name = "charset-normalizer" version = "3.3.2" @@ -2554,4 +2542,4 @@ test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.funct [metadata] lock-version = "2.1" python-versions = "^3.8.6" -content-hash = "614c92ef3194008d18f429a0066a06c66856852e13614528e09f133aec8f59b6" +content-hash = "6699ffd6da6d57d2023c38d8b5b8186c58cf563232eff2e76190a7bcbef4d01a" diff --git a/pyproject.toml b/pyproject.toml index 05c09e8a..86fdee73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ tenacity = "^9.0.0" poetry-core = "^1.9.1" jinja2 = "^3.1.4" requests = "^2.32.3" -charm-refresh = {git = "https://github.com/canonical/charm-refresh", rev = "draft"} +charm-refresh = "^3.0.0.1" [tool.poetry.group.charm-libs.dependencies] # data_platform_libs/v0/data_interfaces.py diff --git a/src/charm.py b/src/charm.py index d3858242..159ba137 100755 --- a/src/charm.py +++ b/src/charm.py @@ -92,9 +92,7 @@ def __init__(self, *args) -> None: try: self.refresh = charm_refresh.Machines( _MachinesRouterRefresh( - workload_name="Router", - refresh_user_docs_url="https://charmhub.io/mysql-router/docs/h-upgrade", - _charm=self, + workload_name="Router", charm_name="mysql-router", _charm=self ) ) except charm_refresh.UnitTearingDown: From 858aa812d888984edfbd1e1125c8e70dd0e65c13 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Wed, 7 May 2025 11:09:41 +0200 Subject: [PATCH 11/51] Update comment --- refresh_versions.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/refresh_versions.toml b/refresh_versions.toml index c805ed08..40d35471 100644 --- a/refresh_versions.toml +++ b/refresh_versions.toml @@ -1,8 +1,7 @@ charm_major = 1 workload = "8.0.41" -# autogenerated -charm = "8.0/1.0.0" +charm = "8.0/1.0.0" # TODO remove [snap] name = "charmed-mysql" From bc6c590f673c9d3cfa53fdf5fd320d9f9b121184 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Wed, 7 May 2025 11:12:50 +0200 Subject: [PATCH 12/51] Check workload version --- src/abstract_charm.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/abstract_charm.py b/src/abstract_charm.py index d0492d5b..4c241624 100644 --- a/src/abstract_charm.py +++ b/src/abstract_charm.py @@ -41,6 +41,7 @@ def is_compatible( old_workload_version: str, new_workload_version: str, ) -> bool: + # Check charm version compatibility if not super().is_compatible( old_charm_version=old_charm_version, new_charm_version=new_charm_version, @@ -48,8 +49,18 @@ def is_compatible( new_workload_version=new_workload_version, ): return False - # TODO: check workload version—prevent downgrade? - return True + + # Check workload version compatibility + old_major, old_minor, old_patch = (int(component) for component in old_workload_version.split(".")) + new_major, new_minor, new_patch = (int(component) for component in new_workload_version.split(".")) + if old_major != new_major: + return False + if new_minor > old_minor: + return True + elif new_minor == old_minor: + return new_patch >= old_patch + else: + return False class MySQLRouterCharm(ops.CharmBase, abc.ABC): From 4d1c20eda77c19c9b2077435186747214225aec1 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Wed, 7 May 2025 11:15:53 +0200 Subject: [PATCH 13/51] Update data-platform-workflows to v32.0.0 --- .github/workflows/approve_renovate_pr.yaml | 2 +- .github/workflows/check_pr.yaml | 2 +- .github/workflows/ci.yaml | 2 +- .github/workflows/promote.yaml | 2 +- .github/workflows/release.yaml | 6 +++--- .github/workflows/sync_docs.yaml | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/approve_renovate_pr.yaml b/.github/workflows/approve_renovate_pr.yaml index 45011157..d25bcafa 100644 --- a/.github/workflows/approve_renovate_pr.yaml +++ b/.github/workflows/approve_renovate_pr.yaml @@ -10,6 +10,6 @@ on: jobs: approve-pr: name: Approve Renovate pull request - uses: canonical/data-platform-workflows/.github/workflows/approve_renovate_pr.yaml@v31.0.1 + uses: canonical/data-platform-workflows/.github/workflows/approve_renovate_pr.yaml@v32.0.0 permissions: pull-requests: write # Needed to approve PR diff --git a/.github/workflows/check_pr.yaml b/.github/workflows/check_pr.yaml index 84c56d20..489b5549 100644 --- a/.github/workflows/check_pr.yaml +++ b/.github/workflows/check_pr.yaml @@ -15,4 +15,4 @@ on: jobs: check-pr: name: Check pull request - uses: canonical/data-platform-workflows/.github/workflows/check_charm_pr.yaml@v31.0.1 + uses: canonical/data-platform-workflows/.github/workflows/check_charm_pr.yaml@v32.0.0 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8b05f897..8b55c382 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -55,7 +55,7 @@ jobs: build: name: Build charm - uses: canonical/data-platform-workflows/.github/workflows/build_charm.yaml@v31.0.1 + uses: canonical/data-platform-workflows/.github/workflows/build_charm.yaml@v32.0.0 integration-test: name: Integration test charm diff --git a/.github/workflows/promote.yaml b/.github/workflows/promote.yaml index 9f05c68c..ffd5ac7e 100644 --- a/.github/workflows/promote.yaml +++ b/.github/workflows/promote.yaml @@ -25,7 +25,7 @@ on: jobs: promote: name: Promote charm - uses: canonical/data-platform-workflows/.github/workflows/_promote_charm.yaml@v31.0.1 + uses: canonical/data-platform-workflows/.github/workflows/_promote_charm.yaml@v32.0.0 with: track: 'dpe' from-risk: ${{ inputs.from-risk }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 2b16e17f..819ff053 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,6 +1,6 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. -name: Release to Charmhub +name: Release to Charmhub edge on: push: @@ -24,9 +24,9 @@ jobs: name: Release charm needs: - ci-tests - uses: canonical/data-platform-workflows/.github/workflows/release_charm.yaml@v31.0.1 + uses: canonical/data-platform-workflows/.github/workflows/release_charm_edge.yaml@v32.0.0 with: - channel: dpe/edge + track: 'dpe' artifact-prefix: ${{ needs.ci-tests.outputs.artifact-prefix }} secrets: charmhub-token: ${{ secrets.CHARMHUB_TOKEN }} diff --git a/.github/workflows/sync_docs.yaml b/.github/workflows/sync_docs.yaml index e22b7025..ef3637fd 100644 --- a/.github/workflows/sync_docs.yaml +++ b/.github/workflows/sync_docs.yaml @@ -10,7 +10,7 @@ on: jobs: sync-docs: name: Sync docs from Discourse - uses: canonical/data-platform-workflows/.github/workflows/sync_docs.yaml@v31.0.1 + uses: canonical/data-platform-workflows/.github/workflows/sync_docs.yaml@v32.0.0 with: reviewers: a-velasco permissions: From 197452c0708d06d491ad98d3c8124f28cda5a9c7 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Wed, 7 May 2025 11:20:08 +0200 Subject: [PATCH 14/51] Set charm refresh compatibility version from git tag --- .github/workflows/release.yaml | 13 +++++++++- charmcraft.yaml | 9 ++++++- poetry.lock | 47 ++++++++++++++++++++++++++++++++-- pyproject.toml | 6 +++++ refresh_versions.toml | 2 -- 5 files changed, 71 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 819ff053..7b2bfd3c 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -13,8 +13,18 @@ on: - '.github/workflows/sync_docs.yaml' jobs: + tag: + name: Create charm refresh compatibility version git tag + uses: canonical/data-platform-workflows/.github/workflows/tag_charm_edge.yaml@v32.0.0 + with: + track: 'dpe' + permissions: + contents: write # Needed to create git tag + ci-tests: name: Tests + needs: + - tag uses: ./.github/workflows/ci.yaml secrets: inherit permissions: @@ -23,10 +33,11 @@ jobs: release: name: Release charm needs: + - tag - ci-tests uses: canonical/data-platform-workflows/.github/workflows/release_charm_edge.yaml@v32.0.0 with: - track: 'dpe' + track: ${{ needs.tag.outputs.track }} artifact-prefix: ${{ needs.ci-tests.outputs.artifact-prefix }} secrets: charmhub-token: ${{ secrets.CHARMHUB_TOKEN }} diff --git a/charmcraft.yaml b/charmcraft.yaml index ea240da2..46f5c163 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -83,10 +83,17 @@ parts: files: plugin: dump source: . + after: + - poetry-deps # Ensure poetry is installed build-packages: - git override-build: | - # TODO: set charm version in refresh_versions.toml + # Set `charm_version` in refresh_versions.toml from git tag + python3 -m venv refresh-version-venv + source refresh-version-venv/bin/activate + poetry install --only build-refresh-version + write-charm-version + craftctl default stage: - LICENSE diff --git a/poetry.lock b/poetry.lock index e38c404f..3abe5547 100644 --- a/poetry.lock +++ b/poetry.lock @@ -340,6 +340,22 @@ packaging = ">=24.1" pyyaml = ">=6.0.2" tomli = ">=2.0.1" +[[package]] +name = "charm-refresh-build-version" +version = "0.1.4" +description = "Write `charm` version in refresh_versions.toml from git tag" +optional = false +python-versions = ">=3.8" +groups = ["build-refresh-version"] +files = [ + {file = "charm_refresh_build_version-0.1.4-py3-none-any.whl", hash = "sha256:07cbef34a334e7c56ed9ab5da59243a5c72b3f00ef64e102b5883ddedf57e1c9"}, + {file = "charm_refresh_build_version-0.1.4.tar.gz", hash = "sha256:0b929c3aaeb222ce75ec6323246af0d6294be94d8643d1e36a06a4dc7ebb0cff"}, +] + +[package.dependencies] +dunamai = ">=1.23.1" +tomlkit = ">=0.13.2" + [[package]] name = "charset-normalizer" version = "3.3.2" @@ -639,6 +655,21 @@ wrapt = ">=1.10,<2" [package.extras] dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] +[[package]] +name = "dunamai" +version = "1.23.2" +description = "Dynamic version generation" +optional = false +python-versions = ">=3.5" +groups = ["build-refresh-version"] +files = [ + {file = "dunamai-1.23.2-py3-none-any.whl", hash = "sha256:ba2e3db8045a5bc5c4b9e2654cac3710a0980ad13e025c80fe640592a8ffab9d"}, + {file = "dunamai-1.23.2.tar.gz", hash = "sha256:df71e6de961f715579252011f94982ca864f2120c195c15122f5fd6ad436682f"}, +] + +[package.dependencies] +packaging = ">=20.9" + [[package]] name = "exceptiongroup" version = "1.2.1" @@ -1409,7 +1440,7 @@ version = "24.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main", "integration", "unit"] +groups = ["main", "build-refresh-version", "integration", "unit"] files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, @@ -2243,6 +2274,18 @@ files = [ ] markers = {integration = "python_version < \"3.11\"", unit = "python_full_version <= \"3.11.0a6\""} +[[package]] +name = "tomlkit" +version = "0.13.2" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.8" +groups = ["build-refresh-version"] +files = [ + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, +] + [[package]] name = "toposort" version = "1.10" @@ -2542,4 +2585,4 @@ test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.funct [metadata] lock-version = "2.1" python-versions = "^3.8.6" -content-hash = "6699ffd6da6d57d2023c38d8b5b8186c58cf563232eff2e76190a7bcbef4d01a" +content-hash = "5a374ed86185ccb5d8ef4345e6e9fccbfc3d33ef7891da28bb567fed0bb52595" diff --git a/pyproject.toml b/pyproject.toml index 86fdee73..46ab8c16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,12 @@ tenacity = "^9.0.0" ops = "^2.9.0" allure-pytest-default-results = "^0.1.2" +[tool.poetry.group.build-refresh-version] +optional = true + +[tool.poetry.group.build-refresh-version.dependencies] +charm-refresh-build-version = "^0.1.1" + [tool.coverage.run] branch = true diff --git a/refresh_versions.toml b/refresh_versions.toml index 40d35471..db16967f 100644 --- a/refresh_versions.toml +++ b/refresh_versions.toml @@ -1,8 +1,6 @@ charm_major = 1 workload = "8.0.41" -charm = "8.0/1.0.0" # TODO remove - [snap] name = "charmed-mysql" From d4de70b6354969cc42d52c64cc54aa38c1da9004 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Wed, 7 May 2025 11:21:31 +0200 Subject: [PATCH 15/51] Format --- src/abstract_charm.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/abstract_charm.py b/src/abstract_charm.py index 4c241624..dc82b673 100644 --- a/src/abstract_charm.py +++ b/src/abstract_charm.py @@ -51,8 +51,12 @@ def is_compatible( return False # Check workload version compatibility - old_major, old_minor, old_patch = (int(component) for component in old_workload_version.split(".")) - new_major, new_minor, new_patch = (int(component) for component in new_workload_version.split(".")) + old_major, old_minor, old_patch = ( + int(component) for component in old_workload_version.split(".") + ) + new_major, new_minor, new_patch = ( + int(component) for component in new_workload_version.split(".") + ) if old_major != new_major: return False if new_minor > old_minor: From 18c529294cf5dfd73fe332106e947afd5265afef Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Wed, 7 May 2025 11:21:36 +0200 Subject: [PATCH 16/51] Revert (for testing) "Set charm refresh compatibility version from git tag" This reverts commit 7d5e84b0d14f4ba2200c10ea8832edccea387d46. --- .github/workflows/release.yaml | 13 +--------- charmcraft.yaml | 9 +------ poetry.lock | 47 ++-------------------------------- pyproject.toml | 6 ----- refresh_versions.toml | 2 ++ 5 files changed, 6 insertions(+), 71 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7b2bfd3c..819ff053 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -13,18 +13,8 @@ on: - '.github/workflows/sync_docs.yaml' jobs: - tag: - name: Create charm refresh compatibility version git tag - uses: canonical/data-platform-workflows/.github/workflows/tag_charm_edge.yaml@v32.0.0 - with: - track: 'dpe' - permissions: - contents: write # Needed to create git tag - ci-tests: name: Tests - needs: - - tag uses: ./.github/workflows/ci.yaml secrets: inherit permissions: @@ -33,11 +23,10 @@ jobs: release: name: Release charm needs: - - tag - ci-tests uses: canonical/data-platform-workflows/.github/workflows/release_charm_edge.yaml@v32.0.0 with: - track: ${{ needs.tag.outputs.track }} + track: 'dpe' artifact-prefix: ${{ needs.ci-tests.outputs.artifact-prefix }} secrets: charmhub-token: ${{ secrets.CHARMHUB_TOKEN }} diff --git a/charmcraft.yaml b/charmcraft.yaml index 46f5c163..ea240da2 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -83,17 +83,10 @@ parts: files: plugin: dump source: . - after: - - poetry-deps # Ensure poetry is installed build-packages: - git override-build: | - # Set `charm_version` in refresh_versions.toml from git tag - python3 -m venv refresh-version-venv - source refresh-version-venv/bin/activate - poetry install --only build-refresh-version - write-charm-version - + # TODO: set charm version in refresh_versions.toml craftctl default stage: - LICENSE diff --git a/poetry.lock b/poetry.lock index 3abe5547..e38c404f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -340,22 +340,6 @@ packaging = ">=24.1" pyyaml = ">=6.0.2" tomli = ">=2.0.1" -[[package]] -name = "charm-refresh-build-version" -version = "0.1.4" -description = "Write `charm` version in refresh_versions.toml from git tag" -optional = false -python-versions = ">=3.8" -groups = ["build-refresh-version"] -files = [ - {file = "charm_refresh_build_version-0.1.4-py3-none-any.whl", hash = "sha256:07cbef34a334e7c56ed9ab5da59243a5c72b3f00ef64e102b5883ddedf57e1c9"}, - {file = "charm_refresh_build_version-0.1.4.tar.gz", hash = "sha256:0b929c3aaeb222ce75ec6323246af0d6294be94d8643d1e36a06a4dc7ebb0cff"}, -] - -[package.dependencies] -dunamai = ">=1.23.1" -tomlkit = ">=0.13.2" - [[package]] name = "charset-normalizer" version = "3.3.2" @@ -655,21 +639,6 @@ wrapt = ">=1.10,<2" [package.extras] dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] -[[package]] -name = "dunamai" -version = "1.23.2" -description = "Dynamic version generation" -optional = false -python-versions = ">=3.5" -groups = ["build-refresh-version"] -files = [ - {file = "dunamai-1.23.2-py3-none-any.whl", hash = "sha256:ba2e3db8045a5bc5c4b9e2654cac3710a0980ad13e025c80fe640592a8ffab9d"}, - {file = "dunamai-1.23.2.tar.gz", hash = "sha256:df71e6de961f715579252011f94982ca864f2120c195c15122f5fd6ad436682f"}, -] - -[package.dependencies] -packaging = ">=20.9" - [[package]] name = "exceptiongroup" version = "1.2.1" @@ -1440,7 +1409,7 @@ version = "24.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main", "build-refresh-version", "integration", "unit"] +groups = ["main", "integration", "unit"] files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, @@ -2274,18 +2243,6 @@ files = [ ] markers = {integration = "python_version < \"3.11\"", unit = "python_full_version <= \"3.11.0a6\""} -[[package]] -name = "tomlkit" -version = "0.13.2" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.8" -groups = ["build-refresh-version"] -files = [ - {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, - {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, -] - [[package]] name = "toposort" version = "1.10" @@ -2585,4 +2542,4 @@ test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.funct [metadata] lock-version = "2.1" python-versions = "^3.8.6" -content-hash = "5a374ed86185ccb5d8ef4345e6e9fccbfc3d33ef7891da28bb567fed0bb52595" +content-hash = "6699ffd6da6d57d2023c38d8b5b8186c58cf563232eff2e76190a7bcbef4d01a" diff --git a/pyproject.toml b/pyproject.toml index 46ab8c16..86fdee73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,12 +56,6 @@ tenacity = "^9.0.0" ops = "^2.9.0" allure-pytest-default-results = "^0.1.2" -[tool.poetry.group.build-refresh-version] -optional = true - -[tool.poetry.group.build-refresh-version.dependencies] -charm-refresh-build-version = "^0.1.1" - [tool.coverage.run] branch = true diff --git a/refresh_versions.toml b/refresh_versions.toml index db16967f..40d35471 100644 --- a/refresh_versions.toml +++ b/refresh_versions.toml @@ -1,6 +1,8 @@ charm_major = 1 workload = "8.0.41" +charm = "8.0/1.0.0" # TODO remove + [snap] name = "charmed-mysql" From 2d5630db490818a64ce73ad66f0c34da6c500959 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Wed, 7 May 2025 12:00:21 +0200 Subject: [PATCH 17/51] Fix unit test mocks --- tests/unit/conftest.py | 33 ++++++++++++++++--- .../test_database_relations.py | 2 +- .../test_database_relations_breaking.py | 2 +- tests/unit/scenario_/test_start.py | 2 +- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 8835afec..05308da9 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,9 +1,12 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. - +import pathlib +import platform import ops import pytest +import tomli + from charms.tempo_coordinator_k8s.v0.charm_tracing import charm_tracing_disabled import snap @@ -29,6 +32,28 @@ def disable_tenacity_retry(monkeypatch): monkeypatch.setattr(f"tenacity.{retry_class}.__call__", lambda *args, **kwargs: False) +class _MockRefresh: + in_progress = False + next_unit_allowed_to_refresh = True + workload_allowed_to_start = True + app_status_higher_priority = None + unit_status_higher_priority = None + + def __init__(self, _, /): + pass + + def update_snap_revision(self): + pass + + @property + def pinned_snap_revision(self): + with pathlib.Path("refresh_versions.toml").open("rb") as file: + return tomli.load(file)["snap"]["revisions"][platform.machine()] + + def unit_status_lower_priority(self, *, workload_is_running=True): + return None + + @pytest.fixture(autouse=True) def patch(monkeypatch): monkeypatch.setattr( @@ -41,9 +66,8 @@ def patch(monkeypatch): "mysql_shell.Shell.get_mysql_router_user_for_unit", lambda *args, **kwargs: None ) monkeypatch.setattr("mysql_shell.Shell.is_router_in_cluster_set", lambda *args, **kwargs: True) - monkeypatch.setattr("upgrade.Upgrade.in_progress", False) - monkeypatch.setattr("upgrade.Upgrade.versions_set", True) - monkeypatch.setattr("upgrade.Upgrade.is_compatible", True) + monkeypatch.setattr("charm_refresh.Machines", _MockRefresh) + monkeypatch.setattr("relations.database_requires.RelationEndpoint.does_relation_exist", lambda *args, **kwargs: True) # flake8: noqa: C901 @@ -102,6 +126,7 @@ def restart(self, services: list[str] = []): "snap.Snap._run_command", lambda *args, **kwargs: "null", # Use "null" for `json.loads()` ) + monkeypatch.setattr("snap.Snap.install", lambda *args, **kwargs: None) monkeypatch.setattr("snap._Path.read_text", lambda *args, **kwargs: "") monkeypatch.setattr("snap._Path.write_text", lambda *args, **kwargs: None) monkeypatch.setattr("snap._Path.unlink", lambda *args, **kwargs: None) diff --git a/tests/unit/scenario_/database_relations/test_database_relations.py b/tests/unit/scenario_/database_relations/test_database_relations.py index b0387bf5..8255e0b3 100644 --- a/tests/unit/scenario_/database_relations/test_database_relations.py +++ b/tests/unit/scenario_/database_relations/test_database_relations.py @@ -23,7 +23,7 @@ def output_states(*, relations: list[scenario.Relation]) -> typing.Iterable[scen """ context = scenario.Context(charm.MachineSubordinateRouterCharm) input_state = scenario.State( - relations=[*relations, scenario.PeerRelation(endpoint="upgrade-version-a")], + relations=[*relations, scenario.PeerRelation(endpoint="refresh-v-three")], leader=True, ) events = [] diff --git a/tests/unit/scenario_/database_relations/test_database_relations_breaking.py b/tests/unit/scenario_/database_relations/test_database_relations_breaking.py index ef32152e..799cd53d 100644 --- a/tests/unit/scenario_/database_relations/test_database_relations_breaking.py +++ b/tests/unit/scenario_/database_relations/test_database_relations_breaking.py @@ -17,7 +17,7 @@ def output_state( ) -> scenario.State: context = scenario.Context(charm.MachineSubordinateRouterCharm) input_state = scenario.State( - relations=[*relations, scenario.PeerRelation(endpoint="upgrade-version-a")], + relations=[*relations, scenario.PeerRelation(endpoint="refresh-v-three")], secrets=secrets, leader=True, ) diff --git a/tests/unit/scenario_/test_start.py b/tests/unit/scenario_/test_start.py index b662c61a..1716ea2b 100644 --- a/tests/unit/scenario_/test_start.py +++ b/tests/unit/scenario_/test_start.py @@ -13,7 +13,7 @@ def test_start_sets_status_if_no_relations(leader): context = scenario.Context(charm.MachineSubordinateRouterCharm) input_state = scenario.State( leader=leader, - relations=[scenario.PeerRelation(endpoint="upgrade-version-a")], + relations=[scenario.PeerRelation(endpoint="refresh-v-three")], ) output_state = context.run("start", input_state) if leader: From 8a063555554435da1d75a73f690b4d21078737c5 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Wed, 7 May 2025 12:10:27 +0200 Subject: [PATCH 18/51] Format --- tests/unit/conftest.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 05308da9..98886251 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -6,7 +6,6 @@ import ops import pytest import tomli - from charms.tempo_coordinator_k8s.v0.charm_tracing import charm_tracing_disabled import snap @@ -67,7 +66,10 @@ def patch(monkeypatch): ) monkeypatch.setattr("mysql_shell.Shell.is_router_in_cluster_set", lambda *args, **kwargs: True) monkeypatch.setattr("charm_refresh.Machines", _MockRefresh) - monkeypatch.setattr("relations.database_requires.RelationEndpoint.does_relation_exist", lambda *args, **kwargs: True) + monkeypatch.setattr( + "relations.database_requires.RelationEndpoint.does_relation_exist", + lambda *args, **kwargs: True, + ) # flake8: noqa: C901 From ba3dc3d7c3c80a76f951f6723f5462789558aa9c Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Wed, 7 May 2025 12:25:32 +0200 Subject: [PATCH 19/51] Increase numprocesses to workaround memory leak --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 53c18624..1afd417a 100644 --- a/tox.ini +++ b/tox.ini @@ -41,7 +41,7 @@ description = Run unit tests commands_pre = poetry install --only main,charm-libs,unit commands = - poetry run pytest --numprocesses=auto --cov=src --ignore={[vars]tests_path}/integration/ {posargs} + poetry run pytest --numprocesses 8 --cov=src --ignore={[vars]tests_path}/integration/ {posargs} [testenv:integration] description = Run integration tests From 8b6e123d6e3af191490e127eac5742d29071abec Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Wed, 7 May 2025 12:49:27 +0200 Subject: [PATCH 20/51] Increase numprocesses to workaround memory leak --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 1afd417a..849829db 100644 --- a/tox.ini +++ b/tox.ini @@ -41,7 +41,7 @@ description = Run unit tests commands_pre = poetry install --only main,charm-libs,unit commands = - poetry run pytest --numprocesses 8 --cov=src --ignore={[vars]tests_path}/integration/ {posargs} + poetry run pytest --numprocesses 120 --cov=src --ignore={[vars]tests_path}/integration/ {posargs} [testenv:integration] description = Run integration tests From ede703a02aea8369bdfef45a4272449ab22c0d83 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Thu, 8 May 2025 12:21:05 +0200 Subject: [PATCH 21/51] Set charm refresh compatibility version from git tag --- .github/workflows/release.yaml | 13 +++++++++- charmcraft.yaml | 10 +++++++- poetry.lock | 47 ++++++++++++++++++++++++++++++++-- pyproject.toml | 6 +++++ refresh_versions.toml | 2 -- 5 files changed, 72 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 819ff053..7b2bfd3c 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -13,8 +13,18 @@ on: - '.github/workflows/sync_docs.yaml' jobs: + tag: + name: Create charm refresh compatibility version git tag + uses: canonical/data-platform-workflows/.github/workflows/tag_charm_edge.yaml@v32.0.0 + with: + track: 'dpe' + permissions: + contents: write # Needed to create git tag + ci-tests: name: Tests + needs: + - tag uses: ./.github/workflows/ci.yaml secrets: inherit permissions: @@ -23,10 +33,11 @@ jobs: release: name: Release charm needs: + - tag - ci-tests uses: canonical/data-platform-workflows/.github/workflows/release_charm_edge.yaml@v32.0.0 with: - track: 'dpe' + track: ${{ needs.tag.outputs.track }} artifact-prefix: ${{ needs.ci-tests.outputs.artifact-prefix }} secrets: charmhub-token: ${{ secrets.CHARMHUB_TOKEN }} diff --git a/charmcraft.yaml b/charmcraft.yaml index ea240da2..5bc60da8 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -83,10 +83,18 @@ parts: files: plugin: dump source: . + after: + - poetry-deps # Ensure poetry is installed build-packages: - git override-build: | - # TODO: set charm version in refresh_versions.toml + # Set `charm_version` in refresh_versions.toml from git tag + # Create venv in `..` so that git working tree is not dirty + python3 -m venv ../refresh-version-venv + source ../refresh-version-venv/bin/activate + poetry install --only build-refresh-version + write-charm-version + craftctl default stage: - LICENSE diff --git a/poetry.lock b/poetry.lock index e38c404f..d25584ae 100644 --- a/poetry.lock +++ b/poetry.lock @@ -340,6 +340,22 @@ packaging = ">=24.1" pyyaml = ">=6.0.2" tomli = ">=2.0.1" +[[package]] +name = "charm-refresh-build-version" +version = "0.2.0" +description = "Write `charm` version in refresh_versions.toml from git tag" +optional = false +python-versions = ">=3.8" +groups = ["build-refresh-version"] +files = [ + {file = "charm_refresh_build_version-0.2.0-py3-none-any.whl", hash = "sha256:5a6965772e74549dddfa91eb6c9114605eb9f437ef98e610d7fb428bbd3a934c"}, + {file = "charm_refresh_build_version-0.2.0.tar.gz", hash = "sha256:1ec97659f669f18fc1ff759b5e535ea4dba716c509116b05d8dc4d1f3e7d49a8"}, +] + +[package.dependencies] +dunamai = ">=1.23.1" +tomlkit = ">=0.13.2" + [[package]] name = "charset-normalizer" version = "3.3.2" @@ -639,6 +655,21 @@ wrapt = ">=1.10,<2" [package.extras] dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] +[[package]] +name = "dunamai" +version = "1.23.2" +description = "Dynamic version generation" +optional = false +python-versions = ">=3.5" +groups = ["build-refresh-version"] +files = [ + {file = "dunamai-1.23.2-py3-none-any.whl", hash = "sha256:ba2e3db8045a5bc5c4b9e2654cac3710a0980ad13e025c80fe640592a8ffab9d"}, + {file = "dunamai-1.23.2.tar.gz", hash = "sha256:df71e6de961f715579252011f94982ca864f2120c195c15122f5fd6ad436682f"}, +] + +[package.dependencies] +packaging = ">=20.9" + [[package]] name = "exceptiongroup" version = "1.2.1" @@ -1409,7 +1440,7 @@ version = "24.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main", "integration", "unit"] +groups = ["main", "build-refresh-version", "integration", "unit"] files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, @@ -2243,6 +2274,18 @@ files = [ ] markers = {integration = "python_version < \"3.11\"", unit = "python_full_version <= \"3.11.0a6\""} +[[package]] +name = "tomlkit" +version = "0.13.2" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.8" +groups = ["build-refresh-version"] +files = [ + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, +] + [[package]] name = "toposort" version = "1.10" @@ -2542,4 +2585,4 @@ test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.funct [metadata] lock-version = "2.1" python-versions = "^3.8.6" -content-hash = "6699ffd6da6d57d2023c38d8b5b8186c58cf563232eff2e76190a7bcbef4d01a" +content-hash = "19e9afc02cb00f5307fc18db2e50ab53a39813756aeddfd0e27b688fa6b316c1" diff --git a/pyproject.toml b/pyproject.toml index 86fdee73..a8f7169d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,12 @@ tenacity = "^9.0.0" ops = "^2.9.0" allure-pytest-default-results = "^0.1.2" +[tool.poetry.group.build-refresh-version] +optional = true + +[tool.poetry.group.build-refresh-version.dependencies] +charm-refresh-build-version = "^0.2.0" + [tool.coverage.run] branch = true diff --git a/refresh_versions.toml b/refresh_versions.toml index 40d35471..db16967f 100644 --- a/refresh_versions.toml +++ b/refresh_versions.toml @@ -1,8 +1,6 @@ charm_major = 1 workload = "8.0.41" -charm = "8.0/1.0.0" # TODO remove - [snap] name = "charmed-mysql" From b8182a2cd8244c848aa8af5e699c5657eb470c27 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Thu, 8 May 2025 12:21:10 +0200 Subject: [PATCH 22/51] Revert (for testing) "Set charm refresh compatibility version from git tag" This reverts commit fb9d59a7a1a9dc6e5f628709217387644922fd1b. --- .github/workflows/release.yaml | 13 +--------- charmcraft.yaml | 10 +------- poetry.lock | 47 ++-------------------------------- pyproject.toml | 6 ----- refresh_versions.toml | 2 ++ 5 files changed, 6 insertions(+), 72 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7b2bfd3c..819ff053 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -13,18 +13,8 @@ on: - '.github/workflows/sync_docs.yaml' jobs: - tag: - name: Create charm refresh compatibility version git tag - uses: canonical/data-platform-workflows/.github/workflows/tag_charm_edge.yaml@v32.0.0 - with: - track: 'dpe' - permissions: - contents: write # Needed to create git tag - ci-tests: name: Tests - needs: - - tag uses: ./.github/workflows/ci.yaml secrets: inherit permissions: @@ -33,11 +23,10 @@ jobs: release: name: Release charm needs: - - tag - ci-tests uses: canonical/data-platform-workflows/.github/workflows/release_charm_edge.yaml@v32.0.0 with: - track: ${{ needs.tag.outputs.track }} + track: 'dpe' artifact-prefix: ${{ needs.ci-tests.outputs.artifact-prefix }} secrets: charmhub-token: ${{ secrets.CHARMHUB_TOKEN }} diff --git a/charmcraft.yaml b/charmcraft.yaml index 5bc60da8..ea240da2 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -83,18 +83,10 @@ parts: files: plugin: dump source: . - after: - - poetry-deps # Ensure poetry is installed build-packages: - git override-build: | - # Set `charm_version` in refresh_versions.toml from git tag - # Create venv in `..` so that git working tree is not dirty - python3 -m venv ../refresh-version-venv - source ../refresh-version-venv/bin/activate - poetry install --only build-refresh-version - write-charm-version - + # TODO: set charm version in refresh_versions.toml craftctl default stage: - LICENSE diff --git a/poetry.lock b/poetry.lock index d25584ae..e38c404f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -340,22 +340,6 @@ packaging = ">=24.1" pyyaml = ">=6.0.2" tomli = ">=2.0.1" -[[package]] -name = "charm-refresh-build-version" -version = "0.2.0" -description = "Write `charm` version in refresh_versions.toml from git tag" -optional = false -python-versions = ">=3.8" -groups = ["build-refresh-version"] -files = [ - {file = "charm_refresh_build_version-0.2.0-py3-none-any.whl", hash = "sha256:5a6965772e74549dddfa91eb6c9114605eb9f437ef98e610d7fb428bbd3a934c"}, - {file = "charm_refresh_build_version-0.2.0.tar.gz", hash = "sha256:1ec97659f669f18fc1ff759b5e535ea4dba716c509116b05d8dc4d1f3e7d49a8"}, -] - -[package.dependencies] -dunamai = ">=1.23.1" -tomlkit = ">=0.13.2" - [[package]] name = "charset-normalizer" version = "3.3.2" @@ -655,21 +639,6 @@ wrapt = ">=1.10,<2" [package.extras] dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] -[[package]] -name = "dunamai" -version = "1.23.2" -description = "Dynamic version generation" -optional = false -python-versions = ">=3.5" -groups = ["build-refresh-version"] -files = [ - {file = "dunamai-1.23.2-py3-none-any.whl", hash = "sha256:ba2e3db8045a5bc5c4b9e2654cac3710a0980ad13e025c80fe640592a8ffab9d"}, - {file = "dunamai-1.23.2.tar.gz", hash = "sha256:df71e6de961f715579252011f94982ca864f2120c195c15122f5fd6ad436682f"}, -] - -[package.dependencies] -packaging = ">=20.9" - [[package]] name = "exceptiongroup" version = "1.2.1" @@ -1440,7 +1409,7 @@ version = "24.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main", "build-refresh-version", "integration", "unit"] +groups = ["main", "integration", "unit"] files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, @@ -2274,18 +2243,6 @@ files = [ ] markers = {integration = "python_version < \"3.11\"", unit = "python_full_version <= \"3.11.0a6\""} -[[package]] -name = "tomlkit" -version = "0.13.2" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.8" -groups = ["build-refresh-version"] -files = [ - {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, - {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, -] - [[package]] name = "toposort" version = "1.10" @@ -2585,4 +2542,4 @@ test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.funct [metadata] lock-version = "2.1" python-versions = "^3.8.6" -content-hash = "19e9afc02cb00f5307fc18db2e50ab53a39813756aeddfd0e27b688fa6b316c1" +content-hash = "6699ffd6da6d57d2023c38d8b5b8186c58cf563232eff2e76190a7bcbef4d01a" diff --git a/pyproject.toml b/pyproject.toml index a8f7169d..86fdee73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,12 +56,6 @@ tenacity = "^9.0.0" ops = "^2.9.0" allure-pytest-default-results = "^0.1.2" -[tool.poetry.group.build-refresh-version] -optional = true - -[tool.poetry.group.build-refresh-version.dependencies] -charm-refresh-build-version = "^0.2.0" - [tool.coverage.run] branch = true diff --git a/refresh_versions.toml b/refresh_versions.toml index db16967f..40d35471 100644 --- a/refresh_versions.toml +++ b/refresh_versions.toml @@ -1,6 +1,8 @@ charm_major = 1 workload = "8.0.41" +charm = "8.0/1.0.0" # TODO remove + [snap] name = "charmed-mysql" From 0b5e374ba16ffb8d42d3d6f11e603f51ab6ad01a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sinclert=20P=C3=A9rez?= Date: Thu, 5 Jun 2025 16:13:11 +0200 Subject: [PATCH 23/51] Revert testing changes --- .github/workflows/release.yaml | 13 +++++++++- charmcraft.yaml | 10 +++++++- poetry.lock | 47 ++++++++++++++++++++++++++++++++-- pyproject.toml | 6 +++++ refresh_versions.toml | 2 -- 5 files changed, 72 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 819ff053..7b2bfd3c 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -13,8 +13,18 @@ on: - '.github/workflows/sync_docs.yaml' jobs: + tag: + name: Create charm refresh compatibility version git tag + uses: canonical/data-platform-workflows/.github/workflows/tag_charm_edge.yaml@v32.0.0 + with: + track: 'dpe' + permissions: + contents: write # Needed to create git tag + ci-tests: name: Tests + needs: + - tag uses: ./.github/workflows/ci.yaml secrets: inherit permissions: @@ -23,10 +33,11 @@ jobs: release: name: Release charm needs: + - tag - ci-tests uses: canonical/data-platform-workflows/.github/workflows/release_charm_edge.yaml@v32.0.0 with: - track: 'dpe' + track: ${{ needs.tag.outputs.track }} artifact-prefix: ${{ needs.ci-tests.outputs.artifact-prefix }} secrets: charmhub-token: ${{ secrets.CHARMHUB_TOKEN }} diff --git a/charmcraft.yaml b/charmcraft.yaml index ea240da2..5bc60da8 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -83,10 +83,18 @@ parts: files: plugin: dump source: . + after: + - poetry-deps # Ensure poetry is installed build-packages: - git override-build: | - # TODO: set charm version in refresh_versions.toml + # Set `charm_version` in refresh_versions.toml from git tag + # Create venv in `..` so that git working tree is not dirty + python3 -m venv ../refresh-version-venv + source ../refresh-version-venv/bin/activate + poetry install --only build-refresh-version + write-charm-version + craftctl default stage: - LICENSE diff --git a/poetry.lock b/poetry.lock index e38c404f..d25584ae 100644 --- a/poetry.lock +++ b/poetry.lock @@ -340,6 +340,22 @@ packaging = ">=24.1" pyyaml = ">=6.0.2" tomli = ">=2.0.1" +[[package]] +name = "charm-refresh-build-version" +version = "0.2.0" +description = "Write `charm` version in refresh_versions.toml from git tag" +optional = false +python-versions = ">=3.8" +groups = ["build-refresh-version"] +files = [ + {file = "charm_refresh_build_version-0.2.0-py3-none-any.whl", hash = "sha256:5a6965772e74549dddfa91eb6c9114605eb9f437ef98e610d7fb428bbd3a934c"}, + {file = "charm_refresh_build_version-0.2.0.tar.gz", hash = "sha256:1ec97659f669f18fc1ff759b5e535ea4dba716c509116b05d8dc4d1f3e7d49a8"}, +] + +[package.dependencies] +dunamai = ">=1.23.1" +tomlkit = ">=0.13.2" + [[package]] name = "charset-normalizer" version = "3.3.2" @@ -639,6 +655,21 @@ wrapt = ">=1.10,<2" [package.extras] dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] +[[package]] +name = "dunamai" +version = "1.23.2" +description = "Dynamic version generation" +optional = false +python-versions = ">=3.5" +groups = ["build-refresh-version"] +files = [ + {file = "dunamai-1.23.2-py3-none-any.whl", hash = "sha256:ba2e3db8045a5bc5c4b9e2654cac3710a0980ad13e025c80fe640592a8ffab9d"}, + {file = "dunamai-1.23.2.tar.gz", hash = "sha256:df71e6de961f715579252011f94982ca864f2120c195c15122f5fd6ad436682f"}, +] + +[package.dependencies] +packaging = ">=20.9" + [[package]] name = "exceptiongroup" version = "1.2.1" @@ -1409,7 +1440,7 @@ version = "24.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main", "integration", "unit"] +groups = ["main", "build-refresh-version", "integration", "unit"] files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, @@ -2243,6 +2274,18 @@ files = [ ] markers = {integration = "python_version < \"3.11\"", unit = "python_full_version <= \"3.11.0a6\""} +[[package]] +name = "tomlkit" +version = "0.13.2" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.8" +groups = ["build-refresh-version"] +files = [ + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, +] + [[package]] name = "toposort" version = "1.10" @@ -2542,4 +2585,4 @@ test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.funct [metadata] lock-version = "2.1" python-versions = "^3.8.6" -content-hash = "6699ffd6da6d57d2023c38d8b5b8186c58cf563232eff2e76190a7bcbef4d01a" +content-hash = "19e9afc02cb00f5307fc18db2e50ab53a39813756aeddfd0e27b688fa6b316c1" diff --git a/pyproject.toml b/pyproject.toml index 86fdee73..a8f7169d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,12 @@ tenacity = "^9.0.0" ops = "^2.9.0" allure-pytest-default-results = "^0.1.2" +[tool.poetry.group.build-refresh-version] +optional = true + +[tool.poetry.group.build-refresh-version.dependencies] +charm-refresh-build-version = "^0.2.0" + [tool.coverage.run] branch = true diff --git a/refresh_versions.toml b/refresh_versions.toml index 40d35471..db16967f 100644 --- a/refresh_versions.toml +++ b/refresh_versions.toml @@ -1,8 +1,6 @@ charm_major = 1 workload = "8.0.41" -charm = "8.0/1.0.0" # TODO remove - [snap] name = "charmed-mysql" From 25eb1eb380ea37de735423603d4a3224930c8089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sinclert=20P=C3=A9rez?= Date: Fri, 6 Jun 2025 13:05:58 +0200 Subject: [PATCH 24/51] Fix unit tests --- poetry.lock | 108 ++++++++++++++++++++------------- pyproject.toml | 5 +- src/snap.py | 133 +++++++++++++++++++++++------------------ tests/unit/conftest.py | 49 +++++++++++++-- 4 files changed, 188 insertions(+), 107 deletions(-) diff --git a/poetry.lock b/poetry.lock index d25584ae..41deb326 100644 --- a/poetry.lock +++ b/poetry.lock @@ -716,6 +716,23 @@ files = [ [package.extras] tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich ; python_version >= \"3.11\""] +[[package]] +name = "filelock" +version = "3.18.0" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.9" +groups = ["unit"] +files = [ + {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, + {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] + [[package]] name = "google-auth" version = "2.31.0" @@ -867,26 +884,6 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.link perf = ["ipython"] testing = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf (>=0.9.2)", "pytest-ruff"] -[[package]] -name = "importlib-resources" -version = "6.4.0" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.8" -groups = ["charm-libs"] -markers = "python_version < \"3.9\"" -files = [ - {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"}, - {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"}, -] - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy ; platform_python_implementation != \"PyPy\"", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] - [[package]] name = "iniconfig" version = "2.0.0" @@ -1008,9 +1005,7 @@ files = [ [package.dependencies] attrs = ">=22.2.0" -importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} jsonschema-specifications = ">=2023.03.6" -pkgutil-resolve-name = {version = ">=1.3.10", markers = "python_version < \"3.9\""} referencing = ">=0.28.4" rpds-py = ">=0.7.1" @@ -1031,7 +1026,6 @@ files = [ ] [package.dependencies] -importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} referencing = ">=0.31.0" [[package]] @@ -1512,19 +1506,6 @@ files = [ {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, ] -[[package]] -name = "pkgutil-resolve-name" -version = "1.3.10" -description = "Resolve a name to an object." -optional = false -python-versions = ">=3.6" -groups = ["charm-libs"] -markers = "python_version < \"3.9\"" -files = [ - {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, - {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, -] - [[package]] name = "pluggy" version = "1.5.0" @@ -2263,16 +2244,57 @@ test = ["pytest", "tornado (>=4.5)", "typeguard"] [[package]] name = "tomli" -version = "2.0.1" +version = "2.2.1" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["main", "integration", "unit"] files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +description = "A lil' TOML writer" +optional = false +python-versions = ">=3.9" +groups = ["unit"] +files = [ + {file = "tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90"}, + {file = "tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021"}, ] -markers = {integration = "python_version < \"3.11\"", unit = "python_full_version <= \"3.11.0a6\""} [[package]] name = "tomlkit" @@ -2584,5 +2606,5 @@ test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.funct [metadata] lock-version = "2.1" -python-versions = "^3.8.6" -content-hash = "19e9afc02cb00f5307fc18db2e50ab53a39813756aeddfd0e27b688fa6b316c1" +python-versions = "^3.9.0" +content-hash = "47776deba8a4867334f1ad7b8ffaad65e292207ec5ad70ad389befaefa4bc4de" diff --git a/pyproject.toml b/pyproject.toml index a8f7169d..f26e64c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ package-mode = false requires-poetry = ">=2.0.0" [tool.poetry.dependencies] -python = "^3.8.6" # ^3.8.6 required by juju +python = "^3.9.0" # ^3.9.0 required by tomli-w # breaking change in ops 2.10.0: https://github.com/canonical/operator/pull/1091#issuecomment-1888644075 ops = "^2.9.0, <2.10" tenacity = "^9.0.0" @@ -42,10 +42,13 @@ ruff = "^0.4.5" codespell = "^2.3.0" [tool.poetry.group.unit.dependencies] +filelock ="^3.18.0" pytest = "^8.2.2" pytest-xdist = "^3.6.1" pytest-cov = "^5.0.0" ops-scenario = "^6.0.3, <6.0.4" # 6.0.4 requires ops >= 2.12 +tomli = "^2.2.1" +tomli-w = "^1.2.0" [tool.poetry.group.integration.dependencies] pytest = "^8.2.2" diff --git a/src/snap.py b/src/snap.py index 656739fb..118f26fa 100644 --- a/src/snap.py +++ b/src/snap.py @@ -21,11 +21,6 @@ logger = logging.getLogger(__name__) -_snap_name = charm_refresh.snap_name() -_snap = snap_lib.SnapCache()[_snap_name] -_installed_by_unit = pathlib.Path( - "/var/snap", _snap_name, "common", "installed_by_mysql_router_charm_unit" -) _UNIX_USERNAME = "snap_daemon" @@ -38,48 +33,60 @@ def _raise_if_snap_installed_not_by_this_charm(*, unit: ops.Unit, model_uuid: st Assumes snap is installed """ + snap_name = charm_refresh.snap_name() + snap_unit_path = pathlib.Path( + "/var/snap", snap_name, "common", "installed_by_mysql_router_charm_unit" + ) + if not ( - _installed_by_unit.exists() - and _installed_by_unit.read_text() == _unique_unit_name(unit=unit, model_uuid=model_uuid) + snap_unit_path.exists() + and snap_unit_path.read_text() == _unique_unit_name(unit=unit, model_uuid=model_uuid) ): # The snap could be in use by another charm (e.g. MySQL Server charm, a different MySQL # Router charm). logger.debug( - f"{_installed_by_unit.exists() and _installed_by_unit.read_text()=} {_unique_unit_name(unit=unit, model_uuid=model_uuid)=}" + f"{snap_unit_path.exists() and snap_unit_path.read_text()=} " + f"{_unique_unit_name(unit=unit, model_uuid=model_uuid)=}" ) - logger.error(f"{_snap_name} snap already installed on machine. Installation aborted") - raise Exception(f"Multiple {_snap_name} snap installs not supported on one machine") + logger.error(f"{snap_name} snap already installed on machine. Installation aborted") + raise Exception(f"Multiple {snap_name} snap installs not supported on one machine") def uninstall(): """Uninstall snap if installed""" - logger.debug(f"Ensuring {_snap_name=} is uninstalled") - _snap.ensure(state=snap_lib.SnapState.Absent) - logger.debug(f"Ensured {_snap_name=} is uninstalled") + snap_name = charm_refresh.snap_name() + snap = snap_lib.SnapCache()[snap_name] + + logger.debug(f"Ensuring {snap_name=} is uninstalled") + snap.ensure(state=snap_lib.SnapState.Absent) + logger.debug(f"Ensured {snap_name=} is uninstalled") class _Path(pathlib.PosixPath, container.Path): def __new__(cls, *args, **kwargs): path = super().__new__(cls, *args, **kwargs) + snap_name = charm_refresh.snap_name() + if args and isinstance(args[0], cls) and (parent_ := args[0]._container_parent): path._container_parent = parent_ else: if str(path).startswith("/etc/mysqlrouter") or str(path).startswith( "/var/lib/mysqlrouter" ): - parent = f"/var/snap/{_snap_name}/current" + parent = f"/var/snap/{snap_name}/current" elif str(path).startswith("/run/mysqlrouter") or str(path).startswith( "/var/log/mysqlrouter" ): - parent = f"/var/snap/{_snap_name}/common" + parent = f"/var/snap/{snap_name}/common" elif str(path).startswith("/tmp"): - parent = f"/tmp/snap-private-tmp/snap.{_snap_name}" + parent = f"/tmp/snap-private-tmp/snap.{snap_name}" else: parent = None if parent: assert str(path).startswith("/") path = super().__new__(cls, parent, path.relative_to("/"), **kwargs) path._container_parent = parent + return path def __truediv__(self, other): @@ -128,42 +135,51 @@ class Snap(container.Container): _EXPORTER_SERVICE_NAME = "mysqlrouter-exporter" def __init__(self, *, unit_name: str) -> None: + self._snap_name = charm_refresh.snap_name() + self._snap_unit_path = pathlib.Path( + "/var/snap", self._snap_name, "common", "installed_by_mysql_router_charm_unit" + ) + super().__init__( - mysql_router_command=f"{_snap_name}.mysqlrouter", - mysql_shell_command=f"{_snap_name}.mysqlsh", - mysql_router_password_command=f"{_snap_name}.mysqlrouter-passwd", + mysql_router_command=f"{self._snap_name}.mysqlrouter", + mysql_shell_command=f"{self._snap_name}.mysqlsh", + mysql_router_password_command=f"{self._snap_name}.mysqlrouter-passwd", unit_name=unit_name, ) + @property + def _snap(self): + return snap_lib.SnapCache()[self._snap_name] + @property def ready(self) -> bool: return True @property def mysql_router_service_enabled(self) -> bool: - return _snap.services[self._SERVICE_NAME]["active"] + return self._snap.services[self._SERVICE_NAME]["active"] @property def mysql_router_exporter_service_enabled(self) -> bool: - return _snap.services[self._EXPORTER_SERVICE_NAME]["active"] + return self._snap.services[self._EXPORTER_SERVICE_NAME]["active"] def update_mysql_router_service(self, *, enabled: bool, tls: bool = None) -> None: super().update_mysql_router_service(enabled=enabled, tls=tls) if tls: - _snap.set({"mysqlrouter.extra-options": f"--extra-config {self.tls_config_file}"}) + self._snap.set({"mysqlrouter.extra-options": f"--extra-config {self.tls_config_file}"}) else: - _snap.unset("mysqlrouter.extra-options") + self._snap.unset("mysqlrouter.extra-options") - router_is_running = _snap.services[self._SERVICE_NAME]["active"] + router_is_running = self._snap.services[self._SERVICE_NAME]["active"] if enabled: if router_is_running: - _snap.restart([self._SERVICE_NAME]) + self._snap.restart([self._SERVICE_NAME]) else: - _snap.start([self._SERVICE_NAME], enable=True) + self._snap.start([self._SERVICE_NAME], enable=True) else: - _snap.stop([self._SERVICE_NAME], disable=True) + self._snap.stop([self._SERVICE_NAME], disable=True) def update_mysql_router_exporter_service( self, @@ -185,7 +201,7 @@ def update_mysql_router_exporter_service( ) if enabled: - _snap.set({ + self._snap.set({ "mysqlrouter-exporter.listen-port": config.listen_port, "mysqlrouter-exporter.user": config.username, "mysqlrouter-exporter.password": config.password, @@ -193,30 +209,34 @@ def update_mysql_router_exporter_service( "mysqlrouter-exporter.service-name": self._unit_name.replace("/", "-"), }) if tls: - _snap.set({ + self._snap.set({ "mysqlrouter.tls-cacert-path": certificate_authority_filename, "mysqlrouter.tls-cert-path": certificate_filename, "mysqlrouter.tls-key-path": key_filename, }) else: - _snap.unset("mysqlrouter.tls-cacert-path") - _snap.unset("mysqlrouter.tls-cert-path") - _snap.unset("mysqlrouter.tls-key-path") - _snap.start([self._EXPORTER_SERVICE_NAME], enable=True) + self._snap.unset("mysqlrouter.tls-cacert-path") + self._snap.unset("mysqlrouter.tls-cert-path") + self._snap.unset("mysqlrouter.tls-key-path") + self._snap.start([self._EXPORTER_SERVICE_NAME], enable=True) else: - _snap.stop([self._EXPORTER_SERVICE_NAME], disable=True) - _snap.unset("mysqlrouter-exporter.listen-port") - _snap.unset("mysqlrouter-exporter.user") - _snap.unset("mysqlrouter-exporter.password") - _snap.unset("mysqlrouter-exporter.url") - _snap.unset("mysqlrouter-exporter.service-name") - _snap.unset("mysqlrouter.tls-cacert-path") - _snap.unset("mysqlrouter.tls-cert-path") - _snap.unset("mysqlrouter.tls-key-path") - - @staticmethod + self._snap.stop([self._EXPORTER_SERVICE_NAME], disable=True) + self._snap.unset("mysqlrouter-exporter.listen-port") + self._snap.unset("mysqlrouter-exporter.user") + self._snap.unset("mysqlrouter-exporter.password") + self._snap.unset("mysqlrouter-exporter.url") + self._snap.unset("mysqlrouter-exporter.service-name") + self._snap.unset("mysqlrouter.tls-cacert-path") + self._snap.unset("mysqlrouter.tls-cert-path") + self._snap.unset("mysqlrouter.tls-key-path") + def install( - *, unit: ops.Unit, model_uuid: str, snap_revision: str, refresh: charm_refresh.Machines + self, + *, + unit: ops.Unit, + model_uuid: str, + snap_revision: str, + refresh: charm_refresh.Machines, ) -> None: """Ensure snap is installed by this charm @@ -226,7 +246,7 @@ def install( Automatically retries if snap installation fails """ unique_unit_name = f"{model_uuid}_{unit.name}" - if _snap.present: + if self._snap.present: _raise_if_snap_installed_not_by_this_charm(unit=unit, model_uuid=model_uuid) return # Install snap @@ -246,16 +266,15 @@ def _set_retry_status(_) -> None: reraise=True, ): with attempt: - _snap.ensure(state=snap_lib.SnapState.Present, revision=snap_revision) + self._snap.ensure(state=snap_lib.SnapState.Present, revision=snap_revision) refresh.update_snap_revision() - _snap.hold() - _installed_by_unit.write_text(unique_unit_name) - logger.debug(f"Wrote {unique_unit_name=} to {_installed_by_unit.name=}") + self._snap.hold() + self._snap_unit_path.write_text(unique_unit_name) + logger.debug(f"Wrote {unique_unit_name=} to {self._snap_unit_path.name=}") logger.info(f"Installed snap revision {repr(snap_revision)}") - @classmethod def refresh( - cls, + self, *, unit: ops.Unit, model_uuid: str, @@ -268,24 +287,24 @@ def refresh( Does not automatically retry if snap installation fails """ - if not _snap.present: - cls.install( + if not self._snap.present: + self.install( unit=unit, model_uuid=model_uuid, snap_revision=snap_revision, refresh=refresh ) return _raise_if_snap_installed_not_by_this_charm(unit=unit, model_uuid=model_uuid) - revision_before_refresh = _snap.revision + revision_before_refresh = self._snap.revision if revision_before_refresh == snap_revision: raise ValueError(f"Cannot refresh snap; {snap_revision=} is already installed") logger.info(f"Refreshing snap to revision {repr(snap_revision)}") unit.status = ops.MaintenanceStatus("Refreshing snap") try: - _snap.ensure(state=snap_lib.SnapState.Present, revision=snap_revision) + self._snap.ensure(state=snap_lib.SnapState.Present, revision=snap_revision) except (snap_lib.SnapError, snap_lib.SnapAPIError): logger.exception("Snap refresh failed") - if _snap.revision == revision_before_refresh: + if self._snap.revision == revision_before_refresh: raise container.RefreshFailed else: refresh.update_snap_revision() diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 98886251..381fff3a 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,14 +1,53 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. + import pathlib import platform +import shutil +import filelock import ops import pytest import tomli +import tomli_w from charms.tempo_coordinator_k8s.v0.charm_tracing import charm_tracing_disabled -import snap + +def _mock_charm_version(origin_path: pathlib.Path, backup_path: pathlib.Path) -> None: + """Add charm version to refresh_versions.toml.""" + shutil.copy(origin_path, backup_path) + + with origin_path.open("rb") as file: + versions = tomli.load(file) + + versions["charm"] = "8.0/0.0.0" + with origin_path.open("wb") as file: + tomli_w.dump(versions, file) + + +@pytest.fixture(scope="session", autouse=True) +def mock_charm_version(tmp_path_factory, worker_id): + origin_path = pathlib.Path("refresh_versions.toml") + backup_path = pathlib.Path("refresh_versions.toml.backup") + + # When not running with pytest-xdist + if worker_id == "master": + _mock_charm_version(origin_path, backup_path) + yield + origin_path.unlink() + shutil.move(backup_path, origin_path) + + # When running with pytest-xdist + else: + root_tmp_dir = tmp_path_factory.getbasetemp().parent + lock_file = root_tmp_dir / "refresh_versions.lock" + + # The first worker to acquire the lock mocks the file + with filelock.FileLock(lock_file): + _mock_charm_version(origin_path, backup_path) + yield + origin_path.unlink() + shutil.move(backup_path, origin_path) @pytest.fixture(autouse=True) @@ -122,12 +161,10 @@ def restart(self, services: list[str] = []): if "mysqlrouter-exporter" in services: self.services["mysqlrouter-exporter"]["active"] = True - monkeypatch.setattr(snap, "_snap", Snap()) + monkeypatch.setattr("snap.Snap._snap", Snap()) - monkeypatch.setattr( - "snap.Snap._run_command", - lambda *args, **kwargs: "null", # Use "null" for `json.loads()` - ) + # Use "null" for `json.loads()` + monkeypatch.setattr("snap.Snap._run_command", lambda *args, **kwargs: "null") monkeypatch.setattr("snap.Snap.install", lambda *args, **kwargs: None) monkeypatch.setattr("snap._Path.read_text", lambda *args, **kwargs: "") monkeypatch.setattr("snap._Path.write_text", lambda *args, **kwargs: None) From b3b182c0fec60fa7ea455e651dae8fd073c9744b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sinclert=20P=C3=A9rez?= Date: Fri, 6 Jun 2025 15:28:08 +0200 Subject: [PATCH 25/51] Fix unit tests [alternative] --- poetry.lock | 74 ++++++++++++++++++++++++------------------ pyproject.toml | 5 +-- tests/unit/conftest.py | 55 +++++++++---------------------- 3 files changed, 59 insertions(+), 75 deletions(-) diff --git a/poetry.lock b/poetry.lock index 41deb326..7b8cb206 100644 --- a/poetry.lock +++ b/poetry.lock @@ -716,23 +716,6 @@ files = [ [package.extras] tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich ; python_version >= \"3.11\""] -[[package]] -name = "filelock" -version = "3.18.0" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.9" -groups = ["unit"] -files = [ - {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, - {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] -typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] - [[package]] name = "google-auth" version = "2.31.0" @@ -884,6 +867,30 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.link perf = ["ipython"] testing = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf (>=0.9.2)", "pytest-ruff"] +[[package]] +name = "importlib-resources" +version = "6.4.5" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.8" +groups = ["charm-libs"] +markers = "python_version < \"3.9\"" +files = [ + {file = "importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717"}, + {file = "importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065"}, +] + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] +type = ["pytest-mypy"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -1005,7 +1012,9 @@ files = [ [package.dependencies] attrs = ">=22.2.0" +importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} jsonschema-specifications = ">=2023.03.6" +pkgutil-resolve-name = {version = ">=1.3.10", markers = "python_version < \"3.9\""} referencing = ">=0.28.4" rpds-py = ">=0.7.1" @@ -1026,6 +1035,7 @@ files = [ ] [package.dependencies] +importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} referencing = ">=0.31.0" [[package]] @@ -1506,6 +1516,19 @@ files = [ {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, ] +[[package]] +name = "pkgutil-resolve-name" +version = "1.3.10" +description = "Resolve a name to an object." +optional = false +python-versions = ">=3.6" +groups = ["charm-libs"] +markers = "python_version < \"3.9\"" +files = [ + {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, + {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, +] + [[package]] name = "pluggy" version = "1.5.0" @@ -2283,18 +2306,7 @@ files = [ {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] - -[[package]] -name = "tomli-w" -version = "1.2.0" -description = "A lil' TOML writer" -optional = false -python-versions = ">=3.9" -groups = ["unit"] -files = [ - {file = "tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90"}, - {file = "tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021"}, -] +markers = {integration = "python_version < \"3.11\"", unit = "python_full_version <= \"3.11.0a6\""} [[package]] name = "tomlkit" @@ -2606,5 +2618,5 @@ test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.funct [metadata] lock-version = "2.1" -python-versions = "^3.9.0" -content-hash = "47776deba8a4867334f1ad7b8ffaad65e292207ec5ad70ad389befaefa4bc4de" +python-versions = "^3.8.6" +content-hash = "19e9afc02cb00f5307fc18db2e50ab53a39813756aeddfd0e27b688fa6b316c1" diff --git a/pyproject.toml b/pyproject.toml index f26e64c0..a8f7169d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ package-mode = false requires-poetry = ">=2.0.0" [tool.poetry.dependencies] -python = "^3.9.0" # ^3.9.0 required by tomli-w +python = "^3.8.6" # ^3.8.6 required by juju # breaking change in ops 2.10.0: https://github.com/canonical/operator/pull/1091#issuecomment-1888644075 ops = "^2.9.0, <2.10" tenacity = "^9.0.0" @@ -42,13 +42,10 @@ ruff = "^0.4.5" codespell = "^2.3.0" [tool.poetry.group.unit.dependencies] -filelock ="^3.18.0" pytest = "^8.2.2" pytest-xdist = "^3.6.1" pytest-cov = "^5.0.0" ops-scenario = "^6.0.3, <6.0.4" # 6.0.4 requires ops >= 2.12 -tomli = "^2.2.1" -tomli-w = "^1.2.0" [tool.poetry.group.integration.dependencies] pytest = "^8.2.2" diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 381fff3a..a9c12d17 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -3,53 +3,13 @@ import pathlib import platform -import shutil -import filelock import ops import pytest import tomli -import tomli_w from charms.tempo_coordinator_k8s.v0.charm_tracing import charm_tracing_disabled -def _mock_charm_version(origin_path: pathlib.Path, backup_path: pathlib.Path) -> None: - """Add charm version to refresh_versions.toml.""" - shutil.copy(origin_path, backup_path) - - with origin_path.open("rb") as file: - versions = tomli.load(file) - - versions["charm"] = "8.0/0.0.0" - with origin_path.open("wb") as file: - tomli_w.dump(versions, file) - - -@pytest.fixture(scope="session", autouse=True) -def mock_charm_version(tmp_path_factory, worker_id): - origin_path = pathlib.Path("refresh_versions.toml") - backup_path = pathlib.Path("refresh_versions.toml.backup") - - # When not running with pytest-xdist - if worker_id == "master": - _mock_charm_version(origin_path, backup_path) - yield - origin_path.unlink() - shutil.move(backup_path, origin_path) - - # When running with pytest-xdist - else: - root_tmp_dir = tmp_path_factory.getbasetemp().parent - lock_file = root_tmp_dir / "refresh_versions.lock" - - # The first worker to acquire the lock mocks the file - with filelock.FileLock(lock_file): - _mock_charm_version(origin_path, backup_path) - yield - origin_path.unlink() - shutil.move(backup_path, origin_path) - - @pytest.fixture(autouse=True) def disable_tenacity_retry(monkeypatch): for retry_class in ( @@ -94,6 +54,20 @@ def unit_status_lower_priority(self, *, workload_is_running=True): @pytest.fixture(autouse=True) def patch(monkeypatch): + def _tomli_load(*args, **kwargs) -> dict: + return { + "charm_major": 1, + "workload": "8.0.0", + "charm": "v8.0/1.0.0", + "snap": { + "name": "charmed-mysql", + "revisions": { + "x86_64": "1", + "aarch64": "1", + }, + }, + } + monkeypatch.setattr( "charm.MachineSubordinateRouterCharm.wait_until_mysql_router_ready", lambda *args, **kwargs: None, @@ -105,6 +79,7 @@ def patch(monkeypatch): ) monkeypatch.setattr("mysql_shell.Shell.is_router_in_cluster_set", lambda *args, **kwargs: True) monkeypatch.setattr("charm_refresh.Machines", _MockRefresh) + monkeypatch.setattr("charm_refresh._main.tomli.load", _tomli_load) monkeypatch.setattr( "relations.database_requires.RelationEndpoint.does_relation_exist", lambda *args, **kwargs: True, From 68208fe4650edeb11377372d35910c92b92b238a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sinclert=20P=C3=A9rez?= Date: Fri, 6 Jun 2025 15:28:33 +0200 Subject: [PATCH 26/51] Fix integration tests --- poetry.lock | 57 +++++------------ pyproject.toml | 4 +- tests/integration/helpers.py | 36 ----------- tests/integration/test_upgrade.py | 101 +++++++++++------------------- 4 files changed, 54 insertions(+), 144 deletions(-) diff --git a/poetry.lock b/poetry.lock index 7b8cb206..24042458 100644 --- a/poetry.lock +++ b/poetry.lock @@ -867,30 +867,6 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.link perf = ["ipython"] testing = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf (>=0.9.2)", "pytest-ruff"] -[[package]] -name = "importlib-resources" -version = "6.4.5" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.8" -groups = ["charm-libs"] -markers = "python_version < \"3.9\"" -files = [ - {file = "importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717"}, - {file = "importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065"}, -] - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] -type = ["pytest-mypy"] - [[package]] name = "iniconfig" version = "2.0.0" @@ -1012,9 +988,7 @@ files = [ [package.dependencies] attrs = ">=22.2.0" -importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} jsonschema-specifications = ">=2023.03.6" -pkgutil-resolve-name = {version = ">=1.3.10", markers = "python_version < \"3.9\""} referencing = ">=0.28.4" rpds-py = ">=0.7.1" @@ -1035,7 +1009,6 @@ files = [ ] [package.dependencies] -importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} referencing = ">=0.31.0" [[package]] @@ -1516,19 +1489,6 @@ files = [ {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, ] -[[package]] -name = "pkgutil-resolve-name" -version = "1.3.10" -description = "Resolve a name to an object." -optional = false -python-versions = ">=3.6" -groups = ["charm-libs"] -markers = "python_version < \"3.9\"" -files = [ - {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, - {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, -] - [[package]] name = "pluggy" version = "1.5.0" @@ -2306,7 +2266,18 @@ files = [ {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] -markers = {integration = "python_version < \"3.11\"", unit = "python_full_version <= \"3.11.0a6\""} + +[[package]] +name = "tomli-w" +version = "1.2.0" +description = "A lil' TOML writer" +optional = false +python-versions = ">=3.9" +groups = ["integration"] +files = [ + {file = "tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90"}, + {file = "tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021"}, +] [[package]] name = "tomlkit" @@ -2618,5 +2589,5 @@ test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.funct [metadata] lock-version = "2.1" -python-versions = "^3.8.6" -content-hash = "19e9afc02cb00f5307fc18db2e50ab53a39813756aeddfd0e27b688fa6b316c1" +python-versions = "^3.9.0" +content-hash = "702fca233089cace59f7d21e864b1ae10e755f849d34ede9ad5a09b45c0220c9" diff --git a/pyproject.toml b/pyproject.toml index a8f7169d..316590d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ package-mode = false requires-poetry = ">=2.0.0" [tool.poetry.dependencies] -python = "^3.8.6" # ^3.8.6 required by juju +python = "^3.9.0" # ^3.9.0 required by tomli-w # breaking change in ops 2.10.0: https://github.com/canonical/operator/pull/1091#issuecomment-1888644075 ops = "^2.9.0, <2.10" tenacity = "^9.0.0" @@ -55,6 +55,8 @@ mysql-connector-python = "~8.0.33" tenacity = "^9.0.0" ops = "^2.9.0" allure-pytest-default-results = "^0.1.2" +tomli = "^2.2.1" +tomli-w = "^1.2.0" [tool.poetry.group.build-refresh-version] optional = true diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 88942f92..6832905b 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -8,7 +8,6 @@ from typing import Dict, List, Optional import tenacity -from juju.model import Model from juju.unit import Unit from pytest_operator.plugin import OpsTest @@ -410,41 +409,6 @@ async def ensure_all_units_continuous_writes_incrementing( last_max_written_value = max_written_value -async def get_workload_version(ops_test: OpsTest, unit_name: str) -> str: - """Get the workload version of the deployed router charm.""" - return_code, output, _ = await ops_test.juju( - "ssh", - unit_name, - "sudo", - "cat", - f"/var/lib/juju/agents/unit-{unit_name.replace('/', '-')}/charm/workload_version", - ) - - assert return_code == 0 - return output.strip() - - -async def get_leader_unit( - ops_test: Optional[OpsTest], app_name: str, model: Optional[Model] = None -) -> Optional[Unit]: - """Get the leader unit of a given application. - - Args: - ops_test: The ops test framework instance - app_name: The name of the application - model: The model to use (overrides ops_test.model) - """ - leader_unit = None - if not model: - model = ops_test.model - for unit in model.applications[app_name].units: - if await unit.is_leader_from_status(): - leader_unit = unit - break - - return leader_unit - - def get_juju_status(model_name: str) -> str: """Return the juju status output. diff --git a/tests/integration/test_upgrade.py b/tests/integration/test_upgrade.py index b932e2be..15d008b1 100644 --- a/tests/integration/test_upgrade.py +++ b/tests/integration/test_upgrade.py @@ -5,26 +5,21 @@ import logging import os import pathlib -import platform -import re import shutil import typing import zipfile import pytest -import tenacity +import tomli +import tomli_w +from packaging.version import Version from pytest_operator.plugin import OpsTest -import snap - from .helpers import ( APPLICATION_DEFAULT_APP_NAME, MYSQL_DEFAULT_APP_NAME, MYSQL_ROUTER_DEFAULT_APP_NAME, ensure_all_units_continuous_writes_incrementing, - get_juju_status, - get_leader_unit, - get_workload_version, ) from .juju_ import run_action @@ -91,9 +86,6 @@ async def test_upgrade_from_edge(ops_test: OpsTest, charm, continuous_writes) -> await ensure_all_units_continuous_writes_incrementing(ops_test) mysql_router_application = ops_test.model.applications[MYSQL_ROUTER_APP_NAME] - mysql_router_unit = mysql_router_application.units[0] - - old_workload_version = await get_workload_version(ops_test, mysql_router_unit.name) logger.info("Build charm locally") global temporary_charm @@ -111,23 +103,26 @@ async def test_upgrade_from_edge(ops_test: OpsTest, charm, continuous_writes) -> lambda: mysql_router_application.status == "blocked", timeout=TIMEOUT ) assert ( - "resume-upgrade" in mysql_router_application.status_message - ), "mysql router application status not indicating that user should resume upgrade" - - for attempt in tenacity.Retrying( - reraise=True, - stop=tenacity.stop_after_delay(SMALL_TIMEOUT), - wait=tenacity.wait_fixed(10), - ): - with attempt: - assert "+testupgrade" in get_juju_status( - ops_test.model.name - ), "None of the units are upgraded" + "resume-refresh" in mysql_router_application.status_message + ), "mysql router application status not indicating that user should resume refresh" + + logger.info("Wait for first unit to upgrade") + async with ops_test.fast_forward("60s"): + await ops_test.model.wait_for_idle( + [MYSQL_ROUTER_APP_NAME], + idle_period=30, + timeout=TIMEOUT, + ) - mysql_router_leader_unit = await get_leader_unit(ops_test, MYSQL_ROUTER_APP_NAME) + # Highest to lowest unit number + refresh_order = sorted( + mysql_router_application.units, + key=lambda unit: int(unit.name.split("/")[1]), + reverse=True, + ) - logger.info("Running resume-upgrade on the mysql router leader unit") - await run_action(mysql_router_leader_unit, "resume-upgrade") + logger.info("Running resume-refresh on the mysql router leader unit") + await run_action(refresh_order[1], "resume-refresh") logger.info("Waiting for upgrade to complete on all units") await ops_test.model.wait_for_idle( @@ -137,14 +132,6 @@ async def test_upgrade_from_edge(ops_test: OpsTest, charm, continuous_writes) -> timeout=UPGRADE_TIMEOUT, ) - workload_version_file = pathlib.Path("workload_version") - repo_workload_version = workload_version_file.read_text().strip() - - for unit in mysql_router_application.units: - workload_version = await get_workload_version(ops_test, unit.name) - assert workload_version == f"{repo_workload_version}+testupgrade" - assert old_workload_version != workload_version - await ensure_all_units_continuous_writes_incrementing(ops_test) await ops_test.model.wait_for_idle( @@ -193,17 +180,6 @@ async def test_fail_and_rollback(ops_test: OpsTest, charm, continuous_writes) -> apps=[MYSQL_ROUTER_APP_NAME], status="active", timeout=TIMEOUT, idle_period=30 ) - workload_version_file = pathlib.Path("workload_version") - repo_workload_version = workload_version_file.read_text().strip() - - for unit in mysql_router_application.units: - charm_workload_version = await get_workload_version(ops_test, unit.name) - assert charm_workload_version == f"{repo_workload_version}+testupgrade" - - await ops_test.model.wait_for_idle( - apps=[MYSQL_ROUTER_APP_NAME], status="active", timeout=TIMEOUT - ) - logger.info("Ensure continuous writes after rollback procedure") await ensure_all_units_continuous_writes_incrementing(ops_test) @@ -216,31 +192,28 @@ def create_valid_upgrade_charm(charm_file: typing.Union[str, pathlib.Path]) -> N Upgrades require a new snap revision to avoid no-oping. """ - workload_version_file = pathlib.Path("workload_version") - workload_version = workload_version_file.read_text().strip() + with pathlib.Path("refresh_versions.toml").open("rb") as file: + versions = tomli.load(file) + + # charm needs to refresh snap to be able to avoid no-op when upgrading. + # set an old revision of the snap + versions["snap"]["revisions"]["x86_64"] = "121" + versions["snap"]["revisions"]["aarch64"] = "122" with zipfile.ZipFile(charm_file, mode="a") as charm_zip: - charm_zip.writestr("workload_version", f"{workload_version}+testupgrade\n") - - # charm needs to refresh snap to be able to avoid no-op when upgrading. - # set an old revision of the snap - snap_file = pathlib.Path("src/snap.py") - content = snap_file.read_text() - old_revision = {"x86_64": "121", "aarch64": "122"}[platform.machine()] - new_snap_content = re.sub( - f'"{platform.machine()}": "{snap.revision}"', - f'"{platform.machine()}": "{old_revision}"', - str(content), - ) - charm_zip.writestr("src/snap.py", new_snap_content) + charm_zip.writestr("refresh_versions.toml", tomli_w.dumps(versions)) def create_invalid_upgrade_charm(charm_file: typing.Union[str, pathlib.Path]) -> None: """Create an invalid mysql router charm for upgrade.""" - workload_version_file = pathlib.Path("workload_version") - old_workload_version = workload_version_file.read_text().strip() - [major, minor, patch] = old_workload_version.split(".") + with pathlib.Path("refresh_versions.toml").open("rb") as file: + versions = tomli.load(file) + + old_version = Version(versions["workload"]) + new_version = Version(f"{old_version.major - 1}.{old_version.minor}.{old_version.micro}") + versions["workload"] = str(new_version) + versions["charm"] = "8.0/0.0.0" with zipfile.ZipFile(charm_file, mode="a") as charm_zip: # an invalid charm version because the major workload_version is one less than the current workload_version - charm_zip.writestr("workload_version", f"{int(major) - 1}.{minor}.{patch}+testrollback\n") + charm_zip.writestr("refresh_versions.toml", tomli_w.dumps(versions)) From c9708adb63acf8e1024180b7cdb4e42c887f8ed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sinclert=20P=C3=A9rez?= Date: Fri, 6 Jun 2025 15:29:01 +0200 Subject: [PATCH 27/51] [Temp] Disable charmcraft version build --- charmcraft.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/charmcraft.yaml b/charmcraft.yaml index 5bc60da8..c4379100 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -90,10 +90,10 @@ parts: override-build: | # Set `charm_version` in refresh_versions.toml from git tag # Create venv in `..` so that git working tree is not dirty - python3 -m venv ../refresh-version-venv - source ../refresh-version-venv/bin/activate - poetry install --only build-refresh-version - write-charm-version + # python3 -m venv ../refresh-version-venv + # source ../refresh-version-venv/bin/activate + # poetry install --only build-refresh-version + # write-charm-version craftctl default stage: From fbafe74acbeb82e3773ada2ca58964518b4f603e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sinclert=20P=C3=A9rez?= Date: Fri, 6 Jun 2025 16:14:25 +0200 Subject: [PATCH 28/51] [Temp] Hard-code charm version --- refresh_versions.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/refresh_versions.toml b/refresh_versions.toml index db16967f..537e8724 100644 --- a/refresh_versions.toml +++ b/refresh_versions.toml @@ -1,6 +1,8 @@ charm_major = 1 workload = "8.0.41" +charm = "8.0/1.1.0" + [snap] name = "charmed-mysql" From 855a7bc95242ad331acc74f16753847744cf051b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sinclert=20P=C3=A9rez?= Date: Mon, 9 Jun 2025 10:02:26 +0200 Subject: [PATCH 29/51] [Temp] Point to a refresh v3 channel --- tests/integration/test_upgrade.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/integration/test_upgrade.py b/tests/integration/test_upgrade.py index 15d008b1..083b763b 100644 --- a/tests/integration/test_upgrade.py +++ b/tests/integration/test_upgrade.py @@ -15,6 +15,7 @@ from packaging.version import Version from pytest_operator.plugin import OpsTest +from . import markers from .helpers import ( APPLICATION_DEFAULT_APP_NAME, MYSQL_DEFAULT_APP_NAME, @@ -34,6 +35,7 @@ TEST_APP_NAME = APPLICATION_DEFAULT_APP_NAME +@markers.amd64_only @pytest.mark.abort_on_fail async def test_deploy_edge(ops_test: OpsTest, series) -> None: """Simple test to ensure that mysql, mysqlrouter and application charms deploy.""" @@ -47,12 +49,15 @@ async def test_deploy_edge(ops_test: OpsTest, series) -> None: config={"profile": "testing"}, series="jammy", ), - ops_test.model.deploy( + ops_test.juju( + "deploy", MYSQL_ROUTER_APP_NAME, - application_name=MYSQL_ROUTER_APP_NAME, - num_units=1, - channel="dpe/edge", - series=series, + "-n", + 1, + "--channel", + "dpe/edge/test-refresh-v3", + "--series", + series, ), ops_test.model.deploy( TEST_APP_NAME, @@ -80,6 +85,7 @@ async def test_deploy_edge(ops_test: OpsTest, series) -> None: ) +@markers.amd64_only @pytest.mark.abort_on_fail async def test_upgrade_from_edge(ops_test: OpsTest, charm, continuous_writes) -> None: """Upgrade mysqlrouter while ensuring continuous writes incrementing.""" @@ -139,6 +145,7 @@ async def test_upgrade_from_edge(ops_test: OpsTest, charm, continuous_writes) -> ) +@markers.amd64_only @pytest.mark.abort_on_fail async def test_fail_and_rollback(ops_test: OpsTest, charm, continuous_writes) -> None: """Upgrade to an invalid version and test rollback. From 01eebb172fdd10d332f6eba2757c8514a49dd1ae Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Fri, 11 Jul 2025 12:31:26 +0200 Subject: [PATCH 30/51] update snap.py --- src/snap.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/snap.py b/src/snap.py index 118f26fa..b4f50b53 100644 --- a/src/snap.py +++ b/src/snap.py @@ -34,18 +34,18 @@ def _raise_if_snap_installed_not_by_this_charm(*, unit: ops.Unit, model_uuid: st Assumes snap is installed """ snap_name = charm_refresh.snap_name() - snap_unit_path = pathlib.Path( + installed_by_unit = pathlib.Path( "/var/snap", snap_name, "common", "installed_by_mysql_router_charm_unit" ) if not ( - snap_unit_path.exists() - and snap_unit_path.read_text() == _unique_unit_name(unit=unit, model_uuid=model_uuid) + installed_by_unit.exists() + and installed_by_unit.read_text() == _unique_unit_name(unit=unit, model_uuid=model_uuid) ): # The snap could be in use by another charm (e.g. MySQL Server charm, a different MySQL # Router charm). logger.debug( - f"{snap_unit_path.exists() and snap_unit_path.read_text()=} " + f"{installed_by_unit.exists() and installed_by_unit.read_text()=} " f"{_unique_unit_name(unit=unit, model_uuid=model_uuid)=}" ) logger.error(f"{snap_name} snap already installed on machine. Installation aborted") @@ -136,7 +136,7 @@ class Snap(container.Container): def __init__(self, *, unit_name: str) -> None: self._snap_name = charm_refresh.snap_name() - self._snap_unit_path = pathlib.Path( + self._installed_by_unit = pathlib.Path( "/var/snap", self._snap_name, "common", "installed_by_mysql_router_charm_unit" ) @@ -269,8 +269,8 @@ def _set_retry_status(_) -> None: self._snap.ensure(state=snap_lib.SnapState.Present, revision=snap_revision) refresh.update_snap_revision() self._snap.hold() - self._snap_unit_path.write_text(unique_unit_name) - logger.debug(f"Wrote {unique_unit_name=} to {self._snap_unit_path.name=}") + self._installed_by_unit.write_text(unique_unit_name) + logger.debug(f"Wrote {unique_unit_name=} to {self._installed_by_unit.name=}") logger.info(f"Installed snap revision {repr(snap_revision)}") def refresh( From 8d4ee797d1fa9c5ff58ae53aaa5824867bb8d5f0 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Fri, 11 Jul 2025 12:31:35 +0200 Subject: [PATCH 31/51] python min version for 22.04 --- poetry.lock | 13 ++++++------- pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/poetry.lock b/poetry.lock index 24042458..c26affb5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. [[package]] name = "allure-pytest" @@ -154,7 +154,7 @@ description = "Base class for creating enumerated constants that are also subcla optional = false python-versions = ">=3.8.6,<3.11" groups = ["integration"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "backports_strenum-1.3.1-py3-none-any.whl", hash = "sha256:cdcfe36dc897e2615dc793b7d3097f54d359918fc448754a517e6f23044ccf83"}, {file = "backports_strenum-1.3.1.tar.gz", hash = "sha256:77c52407342898497714f0596e86188bb7084f89063226f4ba66863482f42414"}, @@ -677,7 +677,7 @@ description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["main", "charm-libs", "integration", "unit"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, @@ -921,7 +921,6 @@ prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0" pygments = ">=2.4.0" stack-data = "*" traitlets = ">=5" -typing-extensions = {version = "*", markers = "python_version < \"3.10\""} [package.extras] all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] @@ -2330,7 +2329,7 @@ files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] -markers = {main = "python_version < \"3.11\""} +markers = {main = "python_version == \"3.10\""} [[package]] name = "typing-inspect" @@ -2589,5 +2588,5 @@ test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.funct [metadata] lock-version = "2.1" -python-versions = "^3.9.0" -content-hash = "702fca233089cace59f7d21e864b1ae10e755f849d34ede9ad5a09b45c0220c9" +python-versions = "^3.10" +content-hash = "d1ee12ce05a76aa6cc7f173f71a9f66588306c4fe38aced7e42d8bd00a6f6063" diff --git a/pyproject.toml b/pyproject.toml index 316590d5..a73c9ed3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ package-mode = false requires-poetry = ">=2.0.0" [tool.poetry.dependencies] -python = "^3.9.0" # ^3.9.0 required by tomli-w +python = "^3.10" # breaking change in ops 2.10.0: https://github.com/canonical/operator/pull/1091#issuecomment-1888644075 ops = "^2.9.0, <2.10" tenacity = "^9.0.0" From 307c520192f053361f187926e147cd4021da0d41 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Fri, 11 Jul 2025 12:42:48 +0200 Subject: [PATCH 32/51] simplify mock to not use charm-refresh private api --- tests/unit/conftest.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index a9c12d17..12a43f0c 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -54,20 +54,6 @@ def unit_status_lower_priority(self, *, workload_is_running=True): @pytest.fixture(autouse=True) def patch(monkeypatch): - def _tomli_load(*args, **kwargs) -> dict: - return { - "charm_major": 1, - "workload": "8.0.0", - "charm": "v8.0/1.0.0", - "snap": { - "name": "charmed-mysql", - "revisions": { - "x86_64": "1", - "aarch64": "1", - }, - }, - } - monkeypatch.setattr( "charm.MachineSubordinateRouterCharm.wait_until_mysql_router_ready", lambda *args, **kwargs: None, @@ -79,7 +65,7 @@ def _tomli_load(*args, **kwargs) -> dict: ) monkeypatch.setattr("mysql_shell.Shell.is_router_in_cluster_set", lambda *args, **kwargs: True) monkeypatch.setattr("charm_refresh.Machines", _MockRefresh) - monkeypatch.setattr("charm_refresh._main.tomli.load", _tomli_load) + monkeypatch.setattr("charm_refresh.snap_name", lambda: "charmed-mysql") monkeypatch.setattr( "relations.database_requires.RelationEndpoint.does_relation_exist", lambda *args, **kwargs: True, From fe6f7c8ec385cbc5d09591039b7436e144c93770 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Fri, 11 Jul 2025 12:48:06 +0200 Subject: [PATCH 33/51] Revert "[Temp] Hard-code charm version" This reverts commit 76505d4f7d21f53f779a2a79c7ded9ce8ddae9b2. --- refresh_versions.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/refresh_versions.toml b/refresh_versions.toml index 537e8724..db16967f 100644 --- a/refresh_versions.toml +++ b/refresh_versions.toml @@ -1,8 +1,6 @@ charm_major = 1 workload = "8.0.41" -charm = "8.0/1.1.0" - [snap] name = "charmed-mysql" From 96b2365dc4ce8ae20e5606c3826498fa60436e60 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Fri, 11 Jul 2025 12:48:06 +0200 Subject: [PATCH 34/51] Revert "[Temp] Disable charmcraft version build" This reverts commit 61b16a8205f318cca20b616888f8b172768a365f. --- charmcraft.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/charmcraft.yaml b/charmcraft.yaml index c4379100..5bc60da8 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -90,10 +90,10 @@ parts: override-build: | # Set `charm_version` in refresh_versions.toml from git tag # Create venv in `..` so that git working tree is not dirty - # python3 -m venv ../refresh-version-venv - # source ../refresh-version-venv/bin/activate - # poetry install --only build-refresh-version - # write-charm-version + python3 -m venv ../refresh-version-venv + source ../refresh-version-venv/bin/activate + poetry install --only build-refresh-version + write-charm-version craftctl default stage: From eebc14a8e68e2554e76dbc3d05dde363d76d2d55 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Fri, 11 Jul 2025 12:49:09 +0200 Subject: [PATCH 35/51] Revert "Revert testing changes" This reverts commit b512ca54b490682a779b13d6beba4b191a8b7c56. --- .github/workflows/release.yaml | 13 +--------- charmcraft.yaml | 10 +------- poetry.lock | 47 ++-------------------------------- pyproject.toml | 6 ----- refresh_versions.toml | 2 ++ 5 files changed, 6 insertions(+), 72 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7b2bfd3c..819ff053 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -13,18 +13,8 @@ on: - '.github/workflows/sync_docs.yaml' jobs: - tag: - name: Create charm refresh compatibility version git tag - uses: canonical/data-platform-workflows/.github/workflows/tag_charm_edge.yaml@v32.0.0 - with: - track: 'dpe' - permissions: - contents: write # Needed to create git tag - ci-tests: name: Tests - needs: - - tag uses: ./.github/workflows/ci.yaml secrets: inherit permissions: @@ -33,11 +23,10 @@ jobs: release: name: Release charm needs: - - tag - ci-tests uses: canonical/data-platform-workflows/.github/workflows/release_charm_edge.yaml@v32.0.0 with: - track: ${{ needs.tag.outputs.track }} + track: 'dpe' artifact-prefix: ${{ needs.ci-tests.outputs.artifact-prefix }} secrets: charmhub-token: ${{ secrets.CHARMHUB_TOKEN }} diff --git a/charmcraft.yaml b/charmcraft.yaml index 5bc60da8..ea240da2 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -83,18 +83,10 @@ parts: files: plugin: dump source: . - after: - - poetry-deps # Ensure poetry is installed build-packages: - git override-build: | - # Set `charm_version` in refresh_versions.toml from git tag - # Create venv in `..` so that git working tree is not dirty - python3 -m venv ../refresh-version-venv - source ../refresh-version-venv/bin/activate - poetry install --only build-refresh-version - write-charm-version - + # TODO: set charm version in refresh_versions.toml craftctl default stage: - LICENSE diff --git a/poetry.lock b/poetry.lock index c26affb5..a2ddbb5b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -340,22 +340,6 @@ packaging = ">=24.1" pyyaml = ">=6.0.2" tomli = ">=2.0.1" -[[package]] -name = "charm-refresh-build-version" -version = "0.2.0" -description = "Write `charm` version in refresh_versions.toml from git tag" -optional = false -python-versions = ">=3.8" -groups = ["build-refresh-version"] -files = [ - {file = "charm_refresh_build_version-0.2.0-py3-none-any.whl", hash = "sha256:5a6965772e74549dddfa91eb6c9114605eb9f437ef98e610d7fb428bbd3a934c"}, - {file = "charm_refresh_build_version-0.2.0.tar.gz", hash = "sha256:1ec97659f669f18fc1ff759b5e535ea4dba716c509116b05d8dc4d1f3e7d49a8"}, -] - -[package.dependencies] -dunamai = ">=1.23.1" -tomlkit = ">=0.13.2" - [[package]] name = "charset-normalizer" version = "3.3.2" @@ -655,21 +639,6 @@ wrapt = ">=1.10,<2" [package.extras] dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] -[[package]] -name = "dunamai" -version = "1.23.2" -description = "Dynamic version generation" -optional = false -python-versions = ">=3.5" -groups = ["build-refresh-version"] -files = [ - {file = "dunamai-1.23.2-py3-none-any.whl", hash = "sha256:ba2e3db8045a5bc5c4b9e2654cac3710a0980ad13e025c80fe640592a8ffab9d"}, - {file = "dunamai-1.23.2.tar.gz", hash = "sha256:df71e6de961f715579252011f94982ca864f2120c195c15122f5fd6ad436682f"}, -] - -[package.dependencies] -packaging = ">=20.9" - [[package]] name = "exceptiongroup" version = "1.2.1" @@ -1416,7 +1385,7 @@ version = "24.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main", "build-refresh-version", "integration", "unit"] +groups = ["main", "integration", "unit"] files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, @@ -2278,18 +2247,6 @@ files = [ {file = "tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021"}, ] -[[package]] -name = "tomlkit" -version = "0.13.2" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.8" -groups = ["build-refresh-version"] -files = [ - {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, - {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, -] - [[package]] name = "toposort" version = "1.10" @@ -2589,4 +2546,4 @@ test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.funct [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "d1ee12ce05a76aa6cc7f173f71a9f66588306c4fe38aced7e42d8bd00a6f6063" +content-hash = "ca2d312e60eea90431168a1a96a1a68c433d3c1582fbeedff6d5e9a67084fe80" diff --git a/pyproject.toml b/pyproject.toml index a73c9ed3..926a71e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,12 +58,6 @@ allure-pytest-default-results = "^0.1.2" tomli = "^2.2.1" tomli-w = "^1.2.0" -[tool.poetry.group.build-refresh-version] -optional = true - -[tool.poetry.group.build-refresh-version.dependencies] -charm-refresh-build-version = "^0.2.0" - [tool.coverage.run] branch = true diff --git a/refresh_versions.toml b/refresh_versions.toml index db16967f..40d35471 100644 --- a/refresh_versions.toml +++ b/refresh_versions.toml @@ -1,6 +1,8 @@ charm_major = 1 workload = "8.0.41" +charm = "8.0/1.0.0" # TODO remove + [snap] name = "charmed-mysql" From 3d0a8fbb53b7993200d5bd65ed6e43eb8be2f333 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Fri, 11 Jul 2025 13:09:58 +0200 Subject: [PATCH 36/51] fix integration test --- tests/integration/test_upgrade.py | 44 ++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/tests/integration/test_upgrade.py b/tests/integration/test_upgrade.py index 083b763b..4a701903 100644 --- a/tests/integration/test_upgrade.py +++ b/tests/integration/test_upgrade.py @@ -12,7 +12,6 @@ import pytest import tomli import tomli_w -from packaging.version import Version from pytest_operator.plugin import OpsTest from . import markers @@ -104,7 +103,35 @@ async def test_upgrade_from_edge(ops_test: OpsTest, charm, continuous_writes) -> logger.info("Refresh the charm") await mysql_router_application.refresh(path=temporary_charm) - logger.info("Wait for the first unit to be refreshed and the app to move to blocked status") + # Refresh will always be incompatible since we are downgrading the workload + # Refresh will additionally be incompatible on PR CI (not edge CI) since unrelease charm + # versions are always marked as incompatible + logger.info("Wait for refresh to block as incompatible") + await ops_test.model.block_until( + lambda: mysql_router_application.status == "blocked", timeout=TIMEOUT + ) + assert ( + "incompatible" in mysql_router_application.status_message + ), "mysql router application status not indicating that refresh incompatible" + + # Highest to lowest unit number + refresh_order = sorted( + mysql_router_application.units, + key=lambda unit: int(unit.name.split("/")[1]), + reverse=True, + ) + + logger.info("Running force-refresh-start action with check-compatibility=false") + await run_action(refresh_order[0], "force-refresh-start", **{"check-compatibility": False}) + + logger.info("Wait for app status to update") + await ops_test.model.wait_for_idle( + [MYSQL_ROUTER_APP_NAME], + idle_period=30, + timeout=60, + ) + + logger.info("Wait for refresh to start") await ops_test.model.block_until( lambda: mysql_router_application.status == "blocked", timeout=TIMEOUT ) @@ -120,14 +147,7 @@ async def test_upgrade_from_edge(ops_test: OpsTest, charm, continuous_writes) -> timeout=TIMEOUT, ) - # Highest to lowest unit number - refresh_order = sorted( - mysql_router_application.units, - key=lambda unit: int(unit.name.split("/")[1]), - reverse=True, - ) - - logger.info("Running resume-refresh on the mysql router leader unit") + logger.info("Running resume-refresh") await run_action(refresh_order[1], "resume-refresh") logger.info("Waiting for upgrade to complete on all units") @@ -206,6 +226,7 @@ def create_valid_upgrade_charm(charm_file: typing.Union[str, pathlib.Path]) -> N # set an old revision of the snap versions["snap"]["revisions"]["x86_64"] = "121" versions["snap"]["revisions"]["aarch64"] = "122" + versions["workload"] = "8.0.39" with zipfile.ZipFile(charm_file, mode="a") as charm_zip: charm_zip.writestr("refresh_versions.toml", tomli_w.dumps(versions)) @@ -216,9 +237,6 @@ def create_invalid_upgrade_charm(charm_file: typing.Union[str, pathlib.Path]) -> with pathlib.Path("refresh_versions.toml").open("rb") as file: versions = tomli.load(file) - old_version = Version(versions["workload"]) - new_version = Version(f"{old_version.major - 1}.{old_version.minor}.{old_version.micro}") - versions["workload"] = str(new_version) versions["charm"] = "8.0/0.0.0" with zipfile.ZipFile(charm_file, mode="a") as charm_zip: From f4e09c5aa01915e491902fad5c94b1d325d75eb4 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Fri, 11 Jul 2025 13:17:49 +0200 Subject: [PATCH 37/51] re-enable arm --- tests/integration/test_upgrade.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/integration/test_upgrade.py b/tests/integration/test_upgrade.py index 4a701903..2c8f5d27 100644 --- a/tests/integration/test_upgrade.py +++ b/tests/integration/test_upgrade.py @@ -34,7 +34,6 @@ TEST_APP_NAME = APPLICATION_DEFAULT_APP_NAME -@markers.amd64_only @pytest.mark.abort_on_fail async def test_deploy_edge(ops_test: OpsTest, series) -> None: """Simple test to ensure that mysql, mysqlrouter and application charms deploy.""" @@ -54,7 +53,7 @@ async def test_deploy_edge(ops_test: OpsTest, series) -> None: "-n", 1, "--channel", - "dpe/edge/test-refresh-v3", + "dpe/edge/test-refresh-v3-8.0.40", # TODO remove after refresh v3 merged "--series", series, ), @@ -84,7 +83,6 @@ async def test_deploy_edge(ops_test: OpsTest, series) -> None: ) -@markers.amd64_only @pytest.mark.abort_on_fail async def test_upgrade_from_edge(ops_test: OpsTest, charm, continuous_writes) -> None: """Upgrade mysqlrouter while ensuring continuous writes incrementing.""" @@ -165,7 +163,6 @@ async def test_upgrade_from_edge(ops_test: OpsTest, charm, continuous_writes) -> ) -@markers.amd64_only @pytest.mark.abort_on_fail async def test_fail_and_rollback(ops_test: OpsTest, charm, continuous_writes) -> None: """Upgrade to an invalid version and test rollback. From 5b9ea38f414d29e19b85313fc84e643273f551d9 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Fri, 11 Jul 2025 13:19:47 +0200 Subject: [PATCH 38/51] format --- tests/integration/test_upgrade.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/integration/test_upgrade.py b/tests/integration/test_upgrade.py index 2c8f5d27..593c49fd 100644 --- a/tests/integration/test_upgrade.py +++ b/tests/integration/test_upgrade.py @@ -14,7 +14,6 @@ import tomli_w from pytest_operator.plugin import OpsTest -from . import markers from .helpers import ( APPLICATION_DEFAULT_APP_NAME, MYSQL_DEFAULT_APP_NAME, @@ -53,7 +52,7 @@ async def test_deploy_edge(ops_test: OpsTest, series) -> None: "-n", 1, "--channel", - "dpe/edge/test-refresh-v3-8.0.40", # TODO remove after refresh v3 merged + "dpe/edge/test-refresh-v3-8.0.40", # TODO remove after refresh v3 merged "--series", series, ), From 92bc960dc91b3ba4b7d2163fd44aaad6253679e5 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Fri, 11 Jul 2025 13:24:00 +0200 Subject: [PATCH 39/51] actions.yaml --- actions.yaml | 4 ++-- refresh_versions.toml | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/actions.yaml b/actions.yaml index 61a57e4f..2a70f26d 100644 --- a/actions.yaml +++ b/actions.yaml @@ -18,7 +18,7 @@ force-refresh-start: description: | Potential of data loss and downtime - If `false`, force refresh if new version of MySQL Router and/or charm is not compatible with previous version + If `false`, force refresh if new version of Router and/or charm is not compatible with previous version run-pre-refresh-checks: type: boolean default: true @@ -32,7 +32,7 @@ force-refresh-start: description: | Potential of data loss and downtime during and after refresh - If `false`, allow refresh to MySQL Router container version that has not been validated to work with the charm revision + If `false`, allow refresh to Router container version that has not been validated to work with the charm revision additionalProperties: false resume-refresh: description: | diff --git a/refresh_versions.toml b/refresh_versions.toml index 40d35471..2e38c8ad 100644 --- a/refresh_versions.toml +++ b/refresh_versions.toml @@ -1,3 +1,5 @@ +# https://canonical-charm-refresh.readthedocs-hosted.com/latest/refresh-versions-toml/ + charm_major = 1 workload = "8.0.41" From c684fc3e3e1de19e5c1e7dc1b3dbb5a26f5d0458 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Fri, 11 Jul 2025 13:24:49 +0200 Subject: [PATCH 40/51] bump charm-refresh --- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index a2ddbb5b..90e14a4a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -320,14 +320,14 @@ charm-api = ">=0.1.1" [[package]] name = "charm-refresh" -version = "3.0.0.1" -description = "In-place rolling refreshes of stateful charmed applications " +version = "3.0.0.6" +description = "In-place rolling refreshes (upgrades) of stateful charmed applications " optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "charm_refresh-3.0.0.1-py3-none-any.whl", hash = "sha256:d67f37c4f66bf94e2c778ef53935c0cc6fb48b6635f58b910130760ba71e7c8d"}, - {file = "charm_refresh-3.0.0.1.tar.gz", hash = "sha256:066bd8a8c96bbfa519f286c8f1ed4d4f80c83b22b8d1929301cba80420bd0079"}, + {file = "charm_refresh-3.0.0.6-py3-none-any.whl", hash = "sha256:36e1736b08358b02833bfa55e21a05d6f9493a447457ebeef82575a599fe7f90"}, + {file = "charm_refresh-3.0.0.6.tar.gz", hash = "sha256:a97fc970f2c1393b749047b5088225acffbd5b4fea1dce493f31e45ebceb4117"}, ] [package.dependencies] @@ -2546,4 +2546,4 @@ test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.funct [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "ca2d312e60eea90431168a1a96a1a68c433d3c1582fbeedff6d5e9a67084fe80" +content-hash = "7b82467d3f357557c751bec9c069d35f3691d59108397936848d2fa3e687dfec" diff --git a/pyproject.toml b/pyproject.toml index 926a71e5..b8802db3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ tenacity = "^9.0.0" poetry-core = "^1.9.1" jinja2 = "^3.1.4" requests = "^2.32.3" -charm-refresh = "^3.0.0.1" +charm-refresh = "^3.0.0.6" [tool.poetry.group.charm-libs.dependencies] # data_platform_libs/v0/data_interfaces.py From 683c4ee3e29e5f18bc12006138275de6bbe3a4c9 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Fri, 11 Jul 2025 13:34:56 +0200 Subject: [PATCH 41/51] try reverting unit test and snap.py changes --- src/snap.py | 133 ++++++++++++++++++----------------------- tests/unit/conftest.py | 11 ++-- 2 files changed, 64 insertions(+), 80 deletions(-) diff --git a/src/snap.py b/src/snap.py index b4f50b53..656739fb 100644 --- a/src/snap.py +++ b/src/snap.py @@ -21,6 +21,11 @@ logger = logging.getLogger(__name__) +_snap_name = charm_refresh.snap_name() +_snap = snap_lib.SnapCache()[_snap_name] +_installed_by_unit = pathlib.Path( + "/var/snap", _snap_name, "common", "installed_by_mysql_router_charm_unit" +) _UNIX_USERNAME = "snap_daemon" @@ -33,60 +38,48 @@ def _raise_if_snap_installed_not_by_this_charm(*, unit: ops.Unit, model_uuid: st Assumes snap is installed """ - snap_name = charm_refresh.snap_name() - installed_by_unit = pathlib.Path( - "/var/snap", snap_name, "common", "installed_by_mysql_router_charm_unit" - ) - if not ( - installed_by_unit.exists() - and installed_by_unit.read_text() == _unique_unit_name(unit=unit, model_uuid=model_uuid) + _installed_by_unit.exists() + and _installed_by_unit.read_text() == _unique_unit_name(unit=unit, model_uuid=model_uuid) ): # The snap could be in use by another charm (e.g. MySQL Server charm, a different MySQL # Router charm). logger.debug( - f"{installed_by_unit.exists() and installed_by_unit.read_text()=} " - f"{_unique_unit_name(unit=unit, model_uuid=model_uuid)=}" + f"{_installed_by_unit.exists() and _installed_by_unit.read_text()=} {_unique_unit_name(unit=unit, model_uuid=model_uuid)=}" ) - logger.error(f"{snap_name} snap already installed on machine. Installation aborted") - raise Exception(f"Multiple {snap_name} snap installs not supported on one machine") + logger.error(f"{_snap_name} snap already installed on machine. Installation aborted") + raise Exception(f"Multiple {_snap_name} snap installs not supported on one machine") def uninstall(): """Uninstall snap if installed""" - snap_name = charm_refresh.snap_name() - snap = snap_lib.SnapCache()[snap_name] - - logger.debug(f"Ensuring {snap_name=} is uninstalled") - snap.ensure(state=snap_lib.SnapState.Absent) - logger.debug(f"Ensured {snap_name=} is uninstalled") + logger.debug(f"Ensuring {_snap_name=} is uninstalled") + _snap.ensure(state=snap_lib.SnapState.Absent) + logger.debug(f"Ensured {_snap_name=} is uninstalled") class _Path(pathlib.PosixPath, container.Path): def __new__(cls, *args, **kwargs): path = super().__new__(cls, *args, **kwargs) - snap_name = charm_refresh.snap_name() - if args and isinstance(args[0], cls) and (parent_ := args[0]._container_parent): path._container_parent = parent_ else: if str(path).startswith("/etc/mysqlrouter") or str(path).startswith( "/var/lib/mysqlrouter" ): - parent = f"/var/snap/{snap_name}/current" + parent = f"/var/snap/{_snap_name}/current" elif str(path).startswith("/run/mysqlrouter") or str(path).startswith( "/var/log/mysqlrouter" ): - parent = f"/var/snap/{snap_name}/common" + parent = f"/var/snap/{_snap_name}/common" elif str(path).startswith("/tmp"): - parent = f"/tmp/snap-private-tmp/snap.{snap_name}" + parent = f"/tmp/snap-private-tmp/snap.{_snap_name}" else: parent = None if parent: assert str(path).startswith("/") path = super().__new__(cls, parent, path.relative_to("/"), **kwargs) path._container_parent = parent - return path def __truediv__(self, other): @@ -135,51 +128,42 @@ class Snap(container.Container): _EXPORTER_SERVICE_NAME = "mysqlrouter-exporter" def __init__(self, *, unit_name: str) -> None: - self._snap_name = charm_refresh.snap_name() - self._installed_by_unit = pathlib.Path( - "/var/snap", self._snap_name, "common", "installed_by_mysql_router_charm_unit" - ) - super().__init__( - mysql_router_command=f"{self._snap_name}.mysqlrouter", - mysql_shell_command=f"{self._snap_name}.mysqlsh", - mysql_router_password_command=f"{self._snap_name}.mysqlrouter-passwd", + mysql_router_command=f"{_snap_name}.mysqlrouter", + mysql_shell_command=f"{_snap_name}.mysqlsh", + mysql_router_password_command=f"{_snap_name}.mysqlrouter-passwd", unit_name=unit_name, ) - @property - def _snap(self): - return snap_lib.SnapCache()[self._snap_name] - @property def ready(self) -> bool: return True @property def mysql_router_service_enabled(self) -> bool: - return self._snap.services[self._SERVICE_NAME]["active"] + return _snap.services[self._SERVICE_NAME]["active"] @property def mysql_router_exporter_service_enabled(self) -> bool: - return self._snap.services[self._EXPORTER_SERVICE_NAME]["active"] + return _snap.services[self._EXPORTER_SERVICE_NAME]["active"] def update_mysql_router_service(self, *, enabled: bool, tls: bool = None) -> None: super().update_mysql_router_service(enabled=enabled, tls=tls) if tls: - self._snap.set({"mysqlrouter.extra-options": f"--extra-config {self.tls_config_file}"}) + _snap.set({"mysqlrouter.extra-options": f"--extra-config {self.tls_config_file}"}) else: - self._snap.unset("mysqlrouter.extra-options") + _snap.unset("mysqlrouter.extra-options") - router_is_running = self._snap.services[self._SERVICE_NAME]["active"] + router_is_running = _snap.services[self._SERVICE_NAME]["active"] if enabled: if router_is_running: - self._snap.restart([self._SERVICE_NAME]) + _snap.restart([self._SERVICE_NAME]) else: - self._snap.start([self._SERVICE_NAME], enable=True) + _snap.start([self._SERVICE_NAME], enable=True) else: - self._snap.stop([self._SERVICE_NAME], disable=True) + _snap.stop([self._SERVICE_NAME], disable=True) def update_mysql_router_exporter_service( self, @@ -201,7 +185,7 @@ def update_mysql_router_exporter_service( ) if enabled: - self._snap.set({ + _snap.set({ "mysqlrouter-exporter.listen-port": config.listen_port, "mysqlrouter-exporter.user": config.username, "mysqlrouter-exporter.password": config.password, @@ -209,34 +193,30 @@ def update_mysql_router_exporter_service( "mysqlrouter-exporter.service-name": self._unit_name.replace("/", "-"), }) if tls: - self._snap.set({ + _snap.set({ "mysqlrouter.tls-cacert-path": certificate_authority_filename, "mysqlrouter.tls-cert-path": certificate_filename, "mysqlrouter.tls-key-path": key_filename, }) else: - self._snap.unset("mysqlrouter.tls-cacert-path") - self._snap.unset("mysqlrouter.tls-cert-path") - self._snap.unset("mysqlrouter.tls-key-path") - self._snap.start([self._EXPORTER_SERVICE_NAME], enable=True) + _snap.unset("mysqlrouter.tls-cacert-path") + _snap.unset("mysqlrouter.tls-cert-path") + _snap.unset("mysqlrouter.tls-key-path") + _snap.start([self._EXPORTER_SERVICE_NAME], enable=True) else: - self._snap.stop([self._EXPORTER_SERVICE_NAME], disable=True) - self._snap.unset("mysqlrouter-exporter.listen-port") - self._snap.unset("mysqlrouter-exporter.user") - self._snap.unset("mysqlrouter-exporter.password") - self._snap.unset("mysqlrouter-exporter.url") - self._snap.unset("mysqlrouter-exporter.service-name") - self._snap.unset("mysqlrouter.tls-cacert-path") - self._snap.unset("mysqlrouter.tls-cert-path") - self._snap.unset("mysqlrouter.tls-key-path") - + _snap.stop([self._EXPORTER_SERVICE_NAME], disable=True) + _snap.unset("mysqlrouter-exporter.listen-port") + _snap.unset("mysqlrouter-exporter.user") + _snap.unset("mysqlrouter-exporter.password") + _snap.unset("mysqlrouter-exporter.url") + _snap.unset("mysqlrouter-exporter.service-name") + _snap.unset("mysqlrouter.tls-cacert-path") + _snap.unset("mysqlrouter.tls-cert-path") + _snap.unset("mysqlrouter.tls-key-path") + + @staticmethod def install( - self, - *, - unit: ops.Unit, - model_uuid: str, - snap_revision: str, - refresh: charm_refresh.Machines, + *, unit: ops.Unit, model_uuid: str, snap_revision: str, refresh: charm_refresh.Machines ) -> None: """Ensure snap is installed by this charm @@ -246,7 +226,7 @@ def install( Automatically retries if snap installation fails """ unique_unit_name = f"{model_uuid}_{unit.name}" - if self._snap.present: + if _snap.present: _raise_if_snap_installed_not_by_this_charm(unit=unit, model_uuid=model_uuid) return # Install snap @@ -266,15 +246,16 @@ def _set_retry_status(_) -> None: reraise=True, ): with attempt: - self._snap.ensure(state=snap_lib.SnapState.Present, revision=snap_revision) + _snap.ensure(state=snap_lib.SnapState.Present, revision=snap_revision) refresh.update_snap_revision() - self._snap.hold() - self._installed_by_unit.write_text(unique_unit_name) - logger.debug(f"Wrote {unique_unit_name=} to {self._installed_by_unit.name=}") + _snap.hold() + _installed_by_unit.write_text(unique_unit_name) + logger.debug(f"Wrote {unique_unit_name=} to {_installed_by_unit.name=}") logger.info(f"Installed snap revision {repr(snap_revision)}") + @classmethod def refresh( - self, + cls, *, unit: ops.Unit, model_uuid: str, @@ -287,24 +268,24 @@ def refresh( Does not automatically retry if snap installation fails """ - if not self._snap.present: - self.install( + if not _snap.present: + cls.install( unit=unit, model_uuid=model_uuid, snap_revision=snap_revision, refresh=refresh ) return _raise_if_snap_installed_not_by_this_charm(unit=unit, model_uuid=model_uuid) - revision_before_refresh = self._snap.revision + revision_before_refresh = _snap.revision if revision_before_refresh == snap_revision: raise ValueError(f"Cannot refresh snap; {snap_revision=} is already installed") logger.info(f"Refreshing snap to revision {repr(snap_revision)}") unit.status = ops.MaintenanceStatus("Refreshing snap") try: - self._snap.ensure(state=snap_lib.SnapState.Present, revision=snap_revision) + _snap.ensure(state=snap_lib.SnapState.Present, revision=snap_revision) except (snap_lib.SnapError, snap_lib.SnapAPIError): logger.exception("Snap refresh failed") - if self._snap.revision == revision_before_refresh: + if _snap.revision == revision_before_refresh: raise container.RefreshFailed else: refresh.update_snap_revision() diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 12a43f0c..6184fed5 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,6 +1,5 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. - import pathlib import platform @@ -9,6 +8,8 @@ import tomli from charms.tempo_coordinator_k8s.v0.charm_tracing import charm_tracing_disabled +import snap + @pytest.fixture(autouse=True) def disable_tenacity_retry(monkeypatch): @@ -122,10 +123,12 @@ def restart(self, services: list[str] = []): if "mysqlrouter-exporter" in services: self.services["mysqlrouter-exporter"]["active"] = True - monkeypatch.setattr("snap.Snap._snap", Snap()) + monkeypatch.setattr(snap, "_snap", Snap()) - # Use "null" for `json.loads()` - monkeypatch.setattr("snap.Snap._run_command", lambda *args, **kwargs: "null") + monkeypatch.setattr( + "snap.Snap._run_command", + lambda *args, **kwargs: "null", # Use "null" for `json.loads()` + ) monkeypatch.setattr("snap.Snap.install", lambda *args, **kwargs: None) monkeypatch.setattr("snap._Path.read_text", lambda *args, **kwargs: "") monkeypatch.setattr("snap._Path.write_text", lambda *args, **kwargs: None) From 101570d4bcc48cd6cc0a48ee0585a546a48e8af8 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Fri, 11 Jul 2025 16:01:07 +0200 Subject: [PATCH 42/51] test 8.0.41 branch --- tests/integration/test_upgrade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_upgrade.py b/tests/integration/test_upgrade.py index 593c49fd..3120ed76 100644 --- a/tests/integration/test_upgrade.py +++ b/tests/integration/test_upgrade.py @@ -52,7 +52,7 @@ async def test_deploy_edge(ops_test: OpsTest, series) -> None: "-n", 1, "--channel", - "dpe/edge/test-refresh-v3-8.0.40", # TODO remove after refresh v3 merged + "dpe/edge/test-refresh-v3-8.0.41", # TODO remove after refresh v3 merged "--series", series, ), From 8ed68c38b40aa976c2563672852f629d3598f621 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Mon, 14 Jul 2025 09:55:32 +0200 Subject: [PATCH 43/51] temp debug --- src/relations/database_requires.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/relations/database_requires.py b/src/relations/database_requires.py index 1e1b3379..106857ab 100644 --- a/src/relations/database_requires.py +++ b/src/relations/database_requires.py @@ -84,6 +84,7 @@ def __init__(self, *, interface: data_interfaces.DatabaseRequires, event) -> Non endpoints = databag["endpoints"].split(",") assert len(endpoints) == 1 endpoint = endpoints[0] + logger.warning(f"FOOBAR: {endpoint}") self.host = endpoint.split(":")[0] self.port = endpoint.split(":")[1] self.username = databag["username"] From b1a475ffd043909b9fdea45b9ac3cf982a4c94f5 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Mon, 14 Jul 2025 12:27:56 +0200 Subject: [PATCH 44/51] Revert "temp debug" This reverts commit f31ea3b61fd38d1bd66f86f821db600bc33c464d. --- src/relations/database_requires.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/relations/database_requires.py b/src/relations/database_requires.py index 106857ab..1e1b3379 100644 --- a/src/relations/database_requires.py +++ b/src/relations/database_requires.py @@ -84,7 +84,6 @@ def __init__(self, *, interface: data_interfaces.DatabaseRequires, event) -> Non endpoints = databag["endpoints"].split(",") assert len(endpoints) == 1 endpoint = endpoints[0] - logger.warning(f"FOOBAR: {endpoint}") self.host = endpoint.split(":")[0] self.port = endpoint.split(":")[1] self.username = databag["username"] From c41c284b3e135428f4a3dfbb52ac29922bb6b775 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Mon, 14 Jul 2025 14:05:09 +0200 Subject: [PATCH 45/51] Revert "try reverting unit test and snap.py changes" This reverts commit da2bb7dc6b2b36af2240f5295168a7e6c059ad37. --- src/snap.py | 133 +++++++++++++++++++++++------------------ tests/unit/conftest.py | 11 ++-- 2 files changed, 80 insertions(+), 64 deletions(-) diff --git a/src/snap.py b/src/snap.py index 656739fb..b4f50b53 100644 --- a/src/snap.py +++ b/src/snap.py @@ -21,11 +21,6 @@ logger = logging.getLogger(__name__) -_snap_name = charm_refresh.snap_name() -_snap = snap_lib.SnapCache()[_snap_name] -_installed_by_unit = pathlib.Path( - "/var/snap", _snap_name, "common", "installed_by_mysql_router_charm_unit" -) _UNIX_USERNAME = "snap_daemon" @@ -38,48 +33,60 @@ def _raise_if_snap_installed_not_by_this_charm(*, unit: ops.Unit, model_uuid: st Assumes snap is installed """ + snap_name = charm_refresh.snap_name() + installed_by_unit = pathlib.Path( + "/var/snap", snap_name, "common", "installed_by_mysql_router_charm_unit" + ) + if not ( - _installed_by_unit.exists() - and _installed_by_unit.read_text() == _unique_unit_name(unit=unit, model_uuid=model_uuid) + installed_by_unit.exists() + and installed_by_unit.read_text() == _unique_unit_name(unit=unit, model_uuid=model_uuid) ): # The snap could be in use by another charm (e.g. MySQL Server charm, a different MySQL # Router charm). logger.debug( - f"{_installed_by_unit.exists() and _installed_by_unit.read_text()=} {_unique_unit_name(unit=unit, model_uuid=model_uuid)=}" + f"{installed_by_unit.exists() and installed_by_unit.read_text()=} " + f"{_unique_unit_name(unit=unit, model_uuid=model_uuid)=}" ) - logger.error(f"{_snap_name} snap already installed on machine. Installation aborted") - raise Exception(f"Multiple {_snap_name} snap installs not supported on one machine") + logger.error(f"{snap_name} snap already installed on machine. Installation aborted") + raise Exception(f"Multiple {snap_name} snap installs not supported on one machine") def uninstall(): """Uninstall snap if installed""" - logger.debug(f"Ensuring {_snap_name=} is uninstalled") - _snap.ensure(state=snap_lib.SnapState.Absent) - logger.debug(f"Ensured {_snap_name=} is uninstalled") + snap_name = charm_refresh.snap_name() + snap = snap_lib.SnapCache()[snap_name] + + logger.debug(f"Ensuring {snap_name=} is uninstalled") + snap.ensure(state=snap_lib.SnapState.Absent) + logger.debug(f"Ensured {snap_name=} is uninstalled") class _Path(pathlib.PosixPath, container.Path): def __new__(cls, *args, **kwargs): path = super().__new__(cls, *args, **kwargs) + snap_name = charm_refresh.snap_name() + if args and isinstance(args[0], cls) and (parent_ := args[0]._container_parent): path._container_parent = parent_ else: if str(path).startswith("/etc/mysqlrouter") or str(path).startswith( "/var/lib/mysqlrouter" ): - parent = f"/var/snap/{_snap_name}/current" + parent = f"/var/snap/{snap_name}/current" elif str(path).startswith("/run/mysqlrouter") or str(path).startswith( "/var/log/mysqlrouter" ): - parent = f"/var/snap/{_snap_name}/common" + parent = f"/var/snap/{snap_name}/common" elif str(path).startswith("/tmp"): - parent = f"/tmp/snap-private-tmp/snap.{_snap_name}" + parent = f"/tmp/snap-private-tmp/snap.{snap_name}" else: parent = None if parent: assert str(path).startswith("/") path = super().__new__(cls, parent, path.relative_to("/"), **kwargs) path._container_parent = parent + return path def __truediv__(self, other): @@ -128,42 +135,51 @@ class Snap(container.Container): _EXPORTER_SERVICE_NAME = "mysqlrouter-exporter" def __init__(self, *, unit_name: str) -> None: + self._snap_name = charm_refresh.snap_name() + self._installed_by_unit = pathlib.Path( + "/var/snap", self._snap_name, "common", "installed_by_mysql_router_charm_unit" + ) + super().__init__( - mysql_router_command=f"{_snap_name}.mysqlrouter", - mysql_shell_command=f"{_snap_name}.mysqlsh", - mysql_router_password_command=f"{_snap_name}.mysqlrouter-passwd", + mysql_router_command=f"{self._snap_name}.mysqlrouter", + mysql_shell_command=f"{self._snap_name}.mysqlsh", + mysql_router_password_command=f"{self._snap_name}.mysqlrouter-passwd", unit_name=unit_name, ) + @property + def _snap(self): + return snap_lib.SnapCache()[self._snap_name] + @property def ready(self) -> bool: return True @property def mysql_router_service_enabled(self) -> bool: - return _snap.services[self._SERVICE_NAME]["active"] + return self._snap.services[self._SERVICE_NAME]["active"] @property def mysql_router_exporter_service_enabled(self) -> bool: - return _snap.services[self._EXPORTER_SERVICE_NAME]["active"] + return self._snap.services[self._EXPORTER_SERVICE_NAME]["active"] def update_mysql_router_service(self, *, enabled: bool, tls: bool = None) -> None: super().update_mysql_router_service(enabled=enabled, tls=tls) if tls: - _snap.set({"mysqlrouter.extra-options": f"--extra-config {self.tls_config_file}"}) + self._snap.set({"mysqlrouter.extra-options": f"--extra-config {self.tls_config_file}"}) else: - _snap.unset("mysqlrouter.extra-options") + self._snap.unset("mysqlrouter.extra-options") - router_is_running = _snap.services[self._SERVICE_NAME]["active"] + router_is_running = self._snap.services[self._SERVICE_NAME]["active"] if enabled: if router_is_running: - _snap.restart([self._SERVICE_NAME]) + self._snap.restart([self._SERVICE_NAME]) else: - _snap.start([self._SERVICE_NAME], enable=True) + self._snap.start([self._SERVICE_NAME], enable=True) else: - _snap.stop([self._SERVICE_NAME], disable=True) + self._snap.stop([self._SERVICE_NAME], disable=True) def update_mysql_router_exporter_service( self, @@ -185,7 +201,7 @@ def update_mysql_router_exporter_service( ) if enabled: - _snap.set({ + self._snap.set({ "mysqlrouter-exporter.listen-port": config.listen_port, "mysqlrouter-exporter.user": config.username, "mysqlrouter-exporter.password": config.password, @@ -193,30 +209,34 @@ def update_mysql_router_exporter_service( "mysqlrouter-exporter.service-name": self._unit_name.replace("/", "-"), }) if tls: - _snap.set({ + self._snap.set({ "mysqlrouter.tls-cacert-path": certificate_authority_filename, "mysqlrouter.tls-cert-path": certificate_filename, "mysqlrouter.tls-key-path": key_filename, }) else: - _snap.unset("mysqlrouter.tls-cacert-path") - _snap.unset("mysqlrouter.tls-cert-path") - _snap.unset("mysqlrouter.tls-key-path") - _snap.start([self._EXPORTER_SERVICE_NAME], enable=True) + self._snap.unset("mysqlrouter.tls-cacert-path") + self._snap.unset("mysqlrouter.tls-cert-path") + self._snap.unset("mysqlrouter.tls-key-path") + self._snap.start([self._EXPORTER_SERVICE_NAME], enable=True) else: - _snap.stop([self._EXPORTER_SERVICE_NAME], disable=True) - _snap.unset("mysqlrouter-exporter.listen-port") - _snap.unset("mysqlrouter-exporter.user") - _snap.unset("mysqlrouter-exporter.password") - _snap.unset("mysqlrouter-exporter.url") - _snap.unset("mysqlrouter-exporter.service-name") - _snap.unset("mysqlrouter.tls-cacert-path") - _snap.unset("mysqlrouter.tls-cert-path") - _snap.unset("mysqlrouter.tls-key-path") - - @staticmethod + self._snap.stop([self._EXPORTER_SERVICE_NAME], disable=True) + self._snap.unset("mysqlrouter-exporter.listen-port") + self._snap.unset("mysqlrouter-exporter.user") + self._snap.unset("mysqlrouter-exporter.password") + self._snap.unset("mysqlrouter-exporter.url") + self._snap.unset("mysqlrouter-exporter.service-name") + self._snap.unset("mysqlrouter.tls-cacert-path") + self._snap.unset("mysqlrouter.tls-cert-path") + self._snap.unset("mysqlrouter.tls-key-path") + def install( - *, unit: ops.Unit, model_uuid: str, snap_revision: str, refresh: charm_refresh.Machines + self, + *, + unit: ops.Unit, + model_uuid: str, + snap_revision: str, + refresh: charm_refresh.Machines, ) -> None: """Ensure snap is installed by this charm @@ -226,7 +246,7 @@ def install( Automatically retries if snap installation fails """ unique_unit_name = f"{model_uuid}_{unit.name}" - if _snap.present: + if self._snap.present: _raise_if_snap_installed_not_by_this_charm(unit=unit, model_uuid=model_uuid) return # Install snap @@ -246,16 +266,15 @@ def _set_retry_status(_) -> None: reraise=True, ): with attempt: - _snap.ensure(state=snap_lib.SnapState.Present, revision=snap_revision) + self._snap.ensure(state=snap_lib.SnapState.Present, revision=snap_revision) refresh.update_snap_revision() - _snap.hold() - _installed_by_unit.write_text(unique_unit_name) - logger.debug(f"Wrote {unique_unit_name=} to {_installed_by_unit.name=}") + self._snap.hold() + self._installed_by_unit.write_text(unique_unit_name) + logger.debug(f"Wrote {unique_unit_name=} to {self._installed_by_unit.name=}") logger.info(f"Installed snap revision {repr(snap_revision)}") - @classmethod def refresh( - cls, + self, *, unit: ops.Unit, model_uuid: str, @@ -268,24 +287,24 @@ def refresh( Does not automatically retry if snap installation fails """ - if not _snap.present: - cls.install( + if not self._snap.present: + self.install( unit=unit, model_uuid=model_uuid, snap_revision=snap_revision, refresh=refresh ) return _raise_if_snap_installed_not_by_this_charm(unit=unit, model_uuid=model_uuid) - revision_before_refresh = _snap.revision + revision_before_refresh = self._snap.revision if revision_before_refresh == snap_revision: raise ValueError(f"Cannot refresh snap; {snap_revision=} is already installed") logger.info(f"Refreshing snap to revision {repr(snap_revision)}") unit.status = ops.MaintenanceStatus("Refreshing snap") try: - _snap.ensure(state=snap_lib.SnapState.Present, revision=snap_revision) + self._snap.ensure(state=snap_lib.SnapState.Present, revision=snap_revision) except (snap_lib.SnapError, snap_lib.SnapAPIError): logger.exception("Snap refresh failed") - if _snap.revision == revision_before_refresh: + if self._snap.revision == revision_before_refresh: raise container.RefreshFailed else: refresh.update_snap_revision() diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 6184fed5..12a43f0c 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,5 +1,6 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. + import pathlib import platform @@ -8,8 +9,6 @@ import tomli from charms.tempo_coordinator_k8s.v0.charm_tracing import charm_tracing_disabled -import snap - @pytest.fixture(autouse=True) def disable_tenacity_retry(monkeypatch): @@ -123,12 +122,10 @@ def restart(self, services: list[str] = []): if "mysqlrouter-exporter" in services: self.services["mysqlrouter-exporter"]["active"] = True - monkeypatch.setattr(snap, "_snap", Snap()) + monkeypatch.setattr("snap.Snap._snap", Snap()) - monkeypatch.setattr( - "snap.Snap._run_command", - lambda *args, **kwargs: "null", # Use "null" for `json.loads()` - ) + # Use "null" for `json.loads()` + monkeypatch.setattr("snap.Snap._run_command", lambda *args, **kwargs: "null") monkeypatch.setattr("snap.Snap.install", lambda *args, **kwargs: None) monkeypatch.setattr("snap._Path.read_text", lambda *args, **kwargs: "") monkeypatch.setattr("snap._Path.write_text", lambda *args, **kwargs: None) From 1b6185c56dfdf6e6b33f6920d5e04788c87ea804 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Mon, 14 Jul 2025 14:10:31 +0200 Subject: [PATCH 46/51] Add missing mock --- tests/unit/conftest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 12a43f0c..4b48db1d 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -66,6 +66,9 @@ def patch(monkeypatch): monkeypatch.setattr("mysql_shell.Shell.is_router_in_cluster_set", lambda *args, **kwargs: True) monkeypatch.setattr("charm_refresh.Machines", _MockRefresh) monkeypatch.setattr("charm_refresh.snap_name", lambda: "charmed-mysql") + monkeypatch.setattr( + "charm_refresh.CharmSpecificCommon.__post_init__", lambda *args, **kwargs: None + ) monkeypatch.setattr( "relations.database_requires.RelationEndpoint.does_relation_exist", lambda *args, **kwargs: True, From ff5604e5c1b79eafb62c5a609d7cc00e89e66828 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Mon, 14 Jul 2025 16:01:04 +0200 Subject: [PATCH 47/51] Fix test timeout --- tests/integration/test_upgrade.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_upgrade.py b/tests/integration/test_upgrade.py index 3120ed76..eb302cde 100644 --- a/tests/integration/test_upgrade.py +++ b/tests/integration/test_upgrade.py @@ -125,12 +125,12 @@ async def test_upgrade_from_edge(ops_test: OpsTest, charm, continuous_writes) -> await ops_test.model.wait_for_idle( [MYSQL_ROUTER_APP_NAME], idle_period=30, - timeout=60, + timeout=TIMEOUT, ) logger.info("Wait for refresh to start") await ops_test.model.block_until( - lambda: mysql_router_application.status == "blocked", timeout=TIMEOUT + lambda: mysql_router_application.status == "blocked", timeout=3*60 ) assert ( "resume-refresh" in mysql_router_application.status_message From 16a3cbbf42668cd3037d9c334558c25eb14cf9f9 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Mon, 14 Jul 2025 16:06:43 +0200 Subject: [PATCH 48/51] Format --- tests/integration/test_upgrade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_upgrade.py b/tests/integration/test_upgrade.py index eb302cde..4b6fc2aa 100644 --- a/tests/integration/test_upgrade.py +++ b/tests/integration/test_upgrade.py @@ -130,7 +130,7 @@ async def test_upgrade_from_edge(ops_test: OpsTest, charm, continuous_writes) -> logger.info("Wait for refresh to start") await ops_test.model.block_until( - lambda: mysql_router_application.status == "blocked", timeout=3*60 + lambda: mysql_router_application.status == "blocked", timeout=3 * 60 ) assert ( "resume-refresh" in mysql_router_application.status_message From ef714086d1222f74678d827fa48425286c0be253 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Mon, 14 Jul 2025 14:05:49 +0200 Subject: [PATCH 49/51] Revert "Revert (for testing) "Set charm refresh compatibility version from git tag"" This reverts commit db55999e6da2060f97dc548c3e56d5b90b91e461. --- .github/workflows/release.yaml | 13 +++++++++- charmcraft.yaml | 10 +++++++- poetry.lock | 47 ++++++++++++++++++++++++++++++++-- pyproject.toml | 6 +++++ refresh_versions.toml | 2 -- 5 files changed, 72 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 819ff053..7b2bfd3c 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -13,8 +13,18 @@ on: - '.github/workflows/sync_docs.yaml' jobs: + tag: + name: Create charm refresh compatibility version git tag + uses: canonical/data-platform-workflows/.github/workflows/tag_charm_edge.yaml@v32.0.0 + with: + track: 'dpe' + permissions: + contents: write # Needed to create git tag + ci-tests: name: Tests + needs: + - tag uses: ./.github/workflows/ci.yaml secrets: inherit permissions: @@ -23,10 +33,11 @@ jobs: release: name: Release charm needs: + - tag - ci-tests uses: canonical/data-platform-workflows/.github/workflows/release_charm_edge.yaml@v32.0.0 with: - track: 'dpe' + track: ${{ needs.tag.outputs.track }} artifact-prefix: ${{ needs.ci-tests.outputs.artifact-prefix }} secrets: charmhub-token: ${{ secrets.CHARMHUB_TOKEN }} diff --git a/charmcraft.yaml b/charmcraft.yaml index ea240da2..5bc60da8 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -83,10 +83,18 @@ parts: files: plugin: dump source: . + after: + - poetry-deps # Ensure poetry is installed build-packages: - git override-build: | - # TODO: set charm version in refresh_versions.toml + # Set `charm_version` in refresh_versions.toml from git tag + # Create venv in `..` so that git working tree is not dirty + python3 -m venv ../refresh-version-venv + source ../refresh-version-venv/bin/activate + poetry install --only build-refresh-version + write-charm-version + craftctl default stage: - LICENSE diff --git a/poetry.lock b/poetry.lock index 90e14a4a..7a63bb50 100644 --- a/poetry.lock +++ b/poetry.lock @@ -340,6 +340,22 @@ packaging = ">=24.1" pyyaml = ">=6.0.2" tomli = ">=2.0.1" +[[package]] +name = "charm-refresh-build-version" +version = "0.2.0" +description = "Write `charm` version in refresh_versions.toml from git tag" +optional = false +python-versions = ">=3.8" +groups = ["build-refresh-version"] +files = [ + {file = "charm_refresh_build_version-0.2.0-py3-none-any.whl", hash = "sha256:5a6965772e74549dddfa91eb6c9114605eb9f437ef98e610d7fb428bbd3a934c"}, + {file = "charm_refresh_build_version-0.2.0.tar.gz", hash = "sha256:1ec97659f669f18fc1ff759b5e535ea4dba716c509116b05d8dc4d1f3e7d49a8"}, +] + +[package.dependencies] +dunamai = ">=1.23.1" +tomlkit = ">=0.13.2" + [[package]] name = "charset-normalizer" version = "3.3.2" @@ -639,6 +655,21 @@ wrapt = ">=1.10,<2" [package.extras] dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] +[[package]] +name = "dunamai" +version = "1.23.2" +description = "Dynamic version generation" +optional = false +python-versions = ">=3.5" +groups = ["build-refresh-version"] +files = [ + {file = "dunamai-1.23.2-py3-none-any.whl", hash = "sha256:ba2e3db8045a5bc5c4b9e2654cac3710a0980ad13e025c80fe640592a8ffab9d"}, + {file = "dunamai-1.23.2.tar.gz", hash = "sha256:df71e6de961f715579252011f94982ca864f2120c195c15122f5fd6ad436682f"}, +] + +[package.dependencies] +packaging = ">=20.9" + [[package]] name = "exceptiongroup" version = "1.2.1" @@ -1385,7 +1416,7 @@ version = "24.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main", "integration", "unit"] +groups = ["main", "build-refresh-version", "integration", "unit"] files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, @@ -2247,6 +2278,18 @@ files = [ {file = "tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021"}, ] +[[package]] +name = "tomlkit" +version = "0.13.2" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.8" +groups = ["build-refresh-version"] +files = [ + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, +] + [[package]] name = "toposort" version = "1.10" @@ -2546,4 +2589,4 @@ test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.funct [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "7b82467d3f357557c751bec9c069d35f3691d59108397936848d2fa3e687dfec" +content-hash = "6d27463ae27a9d37d615e3ed41f02bce55b023bdf6882c8a36d45f1d77da2918" diff --git a/pyproject.toml b/pyproject.toml index b8802db3..3a57f623 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,6 +58,12 @@ allure-pytest-default-results = "^0.1.2" tomli = "^2.2.1" tomli-w = "^1.2.0" +[tool.poetry.group.build-refresh-version] +optional = true + +[tool.poetry.group.build-refresh-version.dependencies] +charm-refresh-build-version = "^0.2.0" + [tool.coverage.run] branch = true diff --git a/refresh_versions.toml b/refresh_versions.toml index 2e38c8ad..c00c8fce 100644 --- a/refresh_versions.toml +++ b/refresh_versions.toml @@ -3,8 +3,6 @@ charm_major = 1 workload = "8.0.41" -charm = "8.0/1.0.0" # TODO remove - [snap] name = "charmed-mysql" From e329db6156ef427ffb7aebc9f05865eeb513a511 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Tue, 15 Jul 2025 11:00:24 +0200 Subject: [PATCH 50/51] update comment --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7b2bfd3c..79831343 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -42,4 +42,4 @@ jobs: secrets: charmhub-token: ${{ secrets.CHARMHUB_TOKEN }} permissions: - contents: write # Needed to create GitHub release + contents: write # Needed to create git tags From c041ae69b7d111b2dec428d4392fe2e4864d4152 Mon Sep 17 00:00:00 2001 From: Carl Csaposs Date: Tue, 15 Jul 2025 11:43:33 +0200 Subject: [PATCH 51/51] Replace "upgrade" with "refresh" --- src/abstract_charm.py | 2 +- src/workload.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/abstract_charm.py b/src/abstract_charm.py index dc82b673..16469a4e 100644 --- a/src/abstract_charm.py +++ b/src/abstract_charm.py @@ -338,7 +338,7 @@ def reconcile(self, event=None) -> None: # noqa: C901 if self._database_requires.is_relation_breaking(event): if self.refresh.in_progress: logger.warning( - "Modifying relations during an upgrade is not supported. The charm may be in a broken, unrecoverable state. Re-deploy the charm" + "Modifying relations during a refresh is not supported. The charm may be in a broken, unrecoverable state. Re-deploy the charm" ) self._database_provides.delete_all_databags() elif ( diff --git a/src/workload.py b/src/workload.py index d2bc01c9..6d55e2b3 100644 --- a/src/workload.py +++ b/src/workload.py @@ -240,16 +240,16 @@ def _router_id(self) -> str: # MySQL Router is bootstrapped without `--directory`—there is one system-wide instance. return f"{socket.getfqdn()}::system" - def _cleanup_after_upgrade_or_potential_container_restart(self) -> None: - """Remove Router user after upgrade or (potential) container restart. + def _cleanup_after_refresh_or_potential_container_restart(self) -> None: + """Remove Router user after refresh or (potential) container restart. (On Kubernetes, storage is not persisted on container restart—MySQL Router's config file is deleted. Therefore, MySQL Router needs to be bootstrapped again.) """ if user_info := self.shell.get_mysql_router_user_for_unit(self._charm.unit.name): - logger.debug("Cleaning up after upgrade or container restart") + logger.debug("Cleaning up after refresh or container restart") self.shell.delete_user(user_info.username) - logger.debug("Cleaned up after upgrade or container restart") + logger.debug("Cleaned up after refresh or container restart") # TODO python3.10 min version: Use `list` instead of `typing.List` def _get_bootstrap_command( @@ -349,7 +349,7 @@ def _restart(self, *, event, tls: bool) -> None: def _enable_router(self, *, event, tls: bool, unit_name: str) -> None: """Enable router after setting up all the necessary prerequisites.""" logger.info("Enabling MySQL Router service") - self._cleanup_after_upgrade_or_potential_container_restart() + self._cleanup_after_refresh_or_potential_container_restart() # create an empty credentials file, if the file does not exist self._container.create_router_rest_api_credentials_file() self._bootstrap_router(event=event, tls=tls) @@ -457,7 +457,7 @@ def refresh( if exporter_enabled: self._disable_exporter() if enabled: - logger.debug("Disabling MySQL Router service before upgrade") + logger.debug("Disabling MySQL Router service before refresh") self._disable_router() try: super().refresh(