From 83793ab630d69af6d2287c0bebefc2f425587476 Mon Sep 17 00:00:00 2001 From: Marcelo Henrique Neppel Date: Mon, 17 Jul 2023 17:23:46 -0300 Subject: [PATCH 01/13] Added initial upgrade implementation --- .github/workflows/ci.yaml | 1 + actions.yaml | 2 + lib/charms/data_platform_libs/v0/upgrade.py | 814 ++++++++++++++++++ lib/charms/grafana_agent/v0/cos_agent.py | 114 ++- metadata.yaml | 2 + src/charm.py | 10 +- src/cluster.py | 15 +- src/constants.py | 9 + src/upgrade.py | 120 +++ tests/integration/conftest.py | 18 + .../application-charm/charmcraft.yaml | 1 + tests/integration/test_upgrade.py | 82 ++ tox.ini | 10 + 13 files changed, 1159 insertions(+), 39 deletions(-) create mode 100644 lib/charms/data_platform_libs/v0/upgrade.py create mode 100644 src/upgrade.py create mode 100644 tests/integration/test_upgrade.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0cdf20a51d..926d605808 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -83,6 +83,7 @@ jobs: - password-rotation-integration - plugins-integration - tls-integration + - upgrade-integration name: ${{ matrix.tox-environments }} needs: - lib-check diff --git a/actions.yaml b/actions.yaml index f1b0f557bd..44231ad4a9 100644 --- a/actions.yaml +++ b/actions.yaml @@ -15,6 +15,8 @@ get-password: Possible values - operator, replication, rewind. list-backups: description: Lists backups in s3 storage. +pre-upgrade-check: + description: Run necessary pre-upgrade checks and preparations before executing a charm refresh. restore: description: Restore a database backup using pgBackRest. S3 credentials are retrieved from a relation with the S3 integrator charm. diff --git a/lib/charms/data_platform_libs/v0/upgrade.py b/lib/charms/data_platform_libs/v0/upgrade.py new file mode 100644 index 0000000000..7db37383e2 --- /dev/null +++ b/lib/charms/data_platform_libs/v0/upgrade.py @@ -0,0 +1,814 @@ +# Copyright 2023 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +r"""Handler for `upgrade` relation events for in-place upgrades on VMs.""" + +import json +import logging +from abc import ABC, abstractmethod +from typing import Iterable, List, Literal, Optional, Tuple + +from ops.charm import ( + ActionEvent, + CharmBase, + CharmEvents, + RelationCreatedEvent, + UpgradeCharmEvent, +) +from ops.framework import EventBase, EventSource, Object +from ops.model import Relation, Unit +from pydantic import BaseModel, root_validator, validator + +# The unique Charmhub library identifier, never change it +LIBID = "156258aefb79435a93d933409a8c8684" + +# Increment this major API version when introducing breaking changes +LIBAPI = 0 + +# Increment this PATCH version before using `charmcraft publish-lib` or reset +# to 0 if you are raising the major API version +LIBPATCH = 6 + +PYDEPS = ["pydantic>=1.10,<2"] + +logger = logging.getLogger(__name__) + +# --- DEPENDENCY RESOLUTION FUNCTIONS --- + + +def build_complete_sem_ver(version: str) -> list[int]: + """Builds complete major.minor.patch version from version string. + + Returns: + List of major.minor.patch version integers + """ + versions = [int(ver) if ver != "*" else 0 for ver in str(version).split(".")] + + # padding with 0s until complete major.minor.patch + return (versions + 3 * [0])[:3] + + +def verify_caret_requirements(version: str, requirement: str) -> bool: + """Verifies version requirements using carats. + + Args: + version: the version currently in use + requirement: the requirement version + + Returns: + True if `version` meets defined `requirement`. Otherwise False + """ + if not requirement.startswith("^"): + return True + + requirement = requirement[1:] + + sem_version = build_complete_sem_ver(version) + sem_requirement = build_complete_sem_ver(requirement) + + # caret uses first non-zero character, not enough to just count '. + max_version_index = requirement.count(".") + for i, semver in enumerate(sem_requirement): + if semver != 0: + max_version_index = i + break + + for i in range(3): + # version higher than first non-zero + if (i < max_version_index) and (sem_version[i] > sem_requirement[i]): + return False + + # version either higher or lower than first non-zero + if (i == max_version_index) and (sem_version[i] != sem_requirement[i]): + return False + + # valid + if (i > max_version_index) and (sem_version[i] > sem_requirement[i]): + return True + + return False + + +def verify_tilde_requirements(version: str, requirement: str) -> bool: + """Verifies version requirements using tildes. + + Args: + version: the version currently in use + requirement: the requirement version + + Returns: + True if `version` meets defined `requirement`. Otherwise False + """ + if not requirement.startswith("~"): + return True + + requirement = requirement[1:] + + sem_version = build_complete_sem_ver(version) + sem_requirement = build_complete_sem_ver(requirement) + + max_version_index = min(1, requirement.count(".")) + + for i in range(3): + # version higher before requirement level + if (i < max_version_index) and (sem_version[i] > sem_requirement[i]): + return False + + # version either higher or lower at requirement level + if (i == max_version_index) and (sem_version[i] != sem_requirement[i]): + return False + + # version lower after requirement level + if (i > max_version_index) and (sem_version[i] < sem_requirement[i]): + return False + + # must be valid + return True + + +def verify_wildcard_requirements(version: str, requirement: str) -> bool: + """Verifies version requirements using wildcards. + + Args: + version: the version currently in use + requirement: the requirement version + + Returns: + True if `version` meets defined `requirement`. Otherwise False + """ + if "*" not in requirement: + return True + + sem_version = build_complete_sem_ver(version) + sem_requirement = build_complete_sem_ver(requirement) + + max_version_index = requirement.count(".") + + for i in range(3): + # version not the same before wildcard + if (i < max_version_index) and (sem_version[i] != sem_requirement[i]): + return False + + # version not higher after wildcard + if (i == max_version_index) and (sem_version[i] < sem_requirement[i]): + return False + + # must be valid + return True + + +def verify_inequality_requirements(version: str, requirement: str) -> bool: + """Verifies version requirements using inequalities. + + Args: + version: the version currently in use + requirement: the requirement version + + Returns: + True if `version` meets defined `requirement`. Otherwise False + """ + if not any(char for char in [">", ">="] if requirement.startswith(char)): + return True + + raw_requirement = requirement.replace(">", "").replace("=", "") + + sem_version = build_complete_sem_ver(version) + sem_requirement = build_complete_sem_ver(raw_requirement) + + max_version_index = raw_requirement.count(".") or 0 + + for i in range(3): + # valid at same requirement level + if ( + (i == max_version_index) + and ("=" in requirement) + and (sem_version[i] == sem_requirement[i]) + ): + return True + + # version not increased at any point + if sem_version[i] < sem_requirement[i]: + return False + + # valid + if sem_version[i] > sem_requirement[i]: + return True + + # must not be valid + return False + + +def verify_requirements(version: str, requirement: str) -> bool: + """Verifies a specified version against defined requirements. + + Supports caret (`^`), tilde (`~`), wildcard (`*`) and greater-than inequalities (`>`, `>=`) + + Args: + version: the version currently in use + requirement: the requirement version + + Returns: + True if `version` meets defined `requirement`. Otherwise False + """ + if not all( + [ + verify_inequality_requirements(version=version, requirement=requirement), + verify_caret_requirements(version=version, requirement=requirement), + verify_tilde_requirements(version=version, requirement=requirement), + verify_wildcard_requirements(version=version, requirement=requirement), + ] + ): + return False + + return True + + +# --- DEPENDENCY MODEL TYPES --- + + +class DependencyModel(BaseModel): + """Manager for a single dependency. + + To be used as part of another model representing a collection of arbitrary dependencies. + + Example:: + + class KafkaDependenciesModel(BaseModel): + kafka_charm: DependencyModel + kafka_service: DependencyModel + + deps = { + "kafka_charm": { + "dependencies": {"zookeeper": ">5"}, + "name": "kafka", + "upgrade_supported": ">5", + "version": "10", + }, + "kafka_service": { + "dependencies": {"zookeeper": "^3.6"}, + "name": "kafka", + "upgrade_supported": "~3.3", + "version": "3.3.2", + }, + } + + model = KafkaDependenciesModel(**deps) # loading dict in to model + + print(model.dict()) # exporting back validated deps + """ + + dependencies: dict[str, str] + name: str + upgrade_supported: str + version: str + + @validator("dependencies", "upgrade_supported", each_item=True) + @classmethod + def dependencies_validator(cls, value): + """Validates values with dependencies for multiple special characters.""" + if isinstance(value, dict): + deps = value.values() + else: + deps = [value] + + chars = ["~", "^", ">", "*"] + + for dep in deps: + if (count := sum([dep.count(char) for char in chars])) != 1: + raise ValueError( + f"Value uses greater than 1 special character (^ ~ > *). Found {count}." + ) + + return value + + @root_validator(skip_on_failure=True) + @classmethod + def version_upgrade_supported_validator(cls, values): + """Validates specified `version` meets `upgrade_supported` requirement.""" + if not verify_requirements( + version=values.get("version"), requirement=values.get("upgrade_supported") + ): + raise ValueError( + f"upgrade_supported value {values.get('upgrade_supported')} greater than version value {values.get('version')} for {values.get('name')}." + ) + + return values + + def can_upgrade(self, dependency: "DependencyModel") -> bool: + """Compares two instances of :class:`DependencyModel` for upgradability. + + Args: + dependency: a dependency model to compare this model against + + Returns: + True if current model can upgrade from dependent model. Otherwise False + """ + return verify_requirements(version=self.version, requirement=dependency.upgrade_supported) + + +# --- CUSTOM EXCEPTIONS --- + + +class UpgradeError(Exception): + """Base class for upgrade related exceptions in the module.""" + + def __init__(self, message: str, cause: Optional[str], resolution: Optional[str]): + super().__init__(message) + self.message = message + self.cause = cause or "" + self.resolution = resolution or "" + + def __repr__(self): + """Representation of the UpgradeError class.""" + return f"{type(self).__module__}.{type(self).__name__} - {str(vars(self))}" + + def __str__(self): + """String representation of the UpgradeError class.""" + return repr(self) + + +class ClusterNotReadyError(UpgradeError): + """Exception flagging that the cluster is not ready to start upgrading. + + For example, if the cluster fails :class:`DataUpgrade._on_pre_upgrade_check_action` + + Args: + message: string message to be logged out + cause: short human-readable description of the cause of the error + resolution: short human-readable instructions for manual error resolution (optional) + """ + + def __init__(self, message: str, cause: str, resolution: Optional[str] = None): + super().__init__(message, cause=cause, resolution=resolution) + + +class VersionError(UpgradeError): + """Exception flagging that the old `version` fails to meet the new `upgrade_supported`s. + + For example, upgrades from version `2.x` --> `4.x`, + but `4.x` only supports upgrading from `3.x` onwards + + Args: + message: string message to be logged out + cause: short human-readable description of the cause of the error + resolution: short human-readable instructions for manual solutions to the error (optional) + """ + + def __init__(self, message: str, cause: str, resolution: Optional[str] = None): + super().__init__(message, cause=cause, resolution=resolution) + + +class DependencyError(UpgradeError): + """Exception flagging that some new `dependency` is not being met. + + For example, new version requires related App version `2.x`, but currently is `1.x` + + Args: + message: string message to be logged out + cause: short human-readable description of the cause of the error + resolution: short human-readable instructions for manual solutions to the error (optional) + """ + + def __init__(self, message: str, cause: str, resolution: Optional[str] = None): + super().__init__(message, cause=cause, resolution=resolution) + + +# --- CUSTOM EVENTS --- + + +class UpgradeGrantedEvent(EventBase): + """Used to tell units that they can process an upgrade. + + Handlers of this event must meet the following: + - SHOULD check for related application deps from :class:`DataUpgrade.dependencies` + - MAY raise :class:`DependencyError` if dependency not met + - MUST update unit `state` after validating the success of the upgrade, calling one of: + - :class:`DataUpgrade.set_unit_failed` if the unit upgrade fails + - :class:`DataUpgrade.set_unit_completed` if the unit upgrade succeeds + - MUST call :class:`DataUpgarde.on_upgrade_changed` on exit so event not lost on leader + """ + + +class UpgradeFinishedEvent(EventBase): + """Used to tell units that they finished the upgrade. + + Handlers of this event must meet the following: + - MUST trigger the upgrade in the next unit by, for example, decrementing the partition + value from the rolling update strategy + - MUST update unit `state` if the previous operation fails, calling + :class:`DataUpgrade.set_unit_failed` + """ + + +class UpgradeEvents(CharmEvents): + """Upgrade events. + + This class defines the events that the lib can emit. + """ + + upgrade_granted = EventSource(UpgradeGrantedEvent) + upgrade_finished = EventSource(UpgradeFinishedEvent) + + +# --- EVENT HANDLER --- + + +class DataUpgrade(Object, ABC): + """Manages `upgrade` relation operators for in-place upgrades.""" + + STATES = ["failed", "idle", "ready", "upgrading", "completed"] + + on = UpgradeEvents() # pyright: ignore [reportGeneralTypeIssues] + + def __init__( + self, + charm: CharmBase, + dependency_model: BaseModel, + relation_name: str = "upgrade", + substrate: Literal["vm", "k8s"] = "vm", + ): + super().__init__(charm, relation_name) + self.charm = charm + self.dependency_model = dependency_model + self.relation_name = relation_name + self.substrate = substrate + self._upgrade_stack = None + + # events + self.framework.observe( + self.charm.on[relation_name].relation_created, self._on_upgrade_created + ) + self.framework.observe( + self.charm.on[relation_name].relation_changed, self.on_upgrade_changed + ) + self.framework.observe(self.charm.on.upgrade_charm, self._on_upgrade_charm) + self.framework.observe(getattr(self.on, "upgrade_granted"), self._on_upgrade_granted) + self.framework.observe(getattr(self.on, "upgrade_finished"), self._on_upgrade_finished) + + # actions + self.framework.observe( + getattr(self.charm.on, "pre_upgrade_check_action"), self._on_pre_upgrade_check_action + ) + + @property + def peer_relation(self) -> Optional[Relation]: + """The upgrade peer relation.""" + return self.charm.model.get_relation(self.relation_name) + + @property + def app_units(self) -> Iterable[Unit]: + """The peer-related units in the application.""" + if not self.peer_relation: + return [] + + return set([self.charm.unit] + list(self.peer_relation.units)) + + @property + def state(self) -> Optional[str]: + """The unit state from the upgrade peer relation.""" + if not self.peer_relation: + return None + + return self.peer_relation.data[self.charm.unit].get("state", None) + + @property + def stored_dependencies(self) -> Optional[BaseModel]: + """The application dependencies from the upgrade peer relation.""" + if not self.peer_relation: + return None + + if not (deps := self.peer_relation.data[self.charm.app].get("dependencies", "")): + return None + + return type(self.dependency_model)(**json.loads(deps)) + + @property + def upgrade_stack(self) -> Optional[List[int]]: + """Gets the upgrade stack from the upgrade peer relation. + + Unit.ids are ordered Last-In-First-Out (LIFO). + i.e unit.id at index `-1` is the first unit to upgrade. + unit.id at index `0` is the last unit to upgrade. + + Returns: + List of integer unit.ids, ordered in upgrade order in a stack + """ + if not self.peer_relation: + return None + + # lazy-load + if self._upgrade_stack is None: + self._upgrade_stack = ( + json.loads(self.peer_relation.data[self.charm.app].get("upgrade-stack", "[]")) + or None + ) + + return self._upgrade_stack + + @upgrade_stack.setter + def upgrade_stack(self, stack: List[int]) -> None: + """Sets the upgrade stack to the upgrade peer relation. + + Unit.ids are ordered Last-In-First-Out (LIFO). + i.e unit.id at index `-1` is the first unit to upgrade. + unit.id at index `0` is the last unit to upgrade. + """ + if not self.peer_relation: + return + + self.peer_relation.data[self.charm.app].update({"upgrade-stack": json.dumps(stack)}) + self._upgrade_stack = stack + + @property + def cluster_state(self) -> Optional[str]: + """Current upgrade state for cluster units. + + Determined from :class:`DataUpgrade.STATE`, taking the lowest ordinal unit state. + + For example, if units in have states: `["ready", "upgrading", "completed"]`, + the overall state for the cluster is `ready`. + + Returns: + String of upgrade state from the furthest behind unit. + """ + if not self.peer_relation: + return None + + states = [self.peer_relation.data[unit].get("state", "") for unit in self.app_units] + + try: + return sorted(states, key=self.STATES.index)[0] + except (ValueError, KeyError): + return None + + @abstractmethod + def pre_upgrade_check(self) -> None: + """Runs necessary checks validating the cluster is in a healthy state to upgrade. + + Called by all units during :meth:`_on_pre_upgrade_check_action`. + + Raises: + :class:`ClusterNotReadyError`: if cluster is not ready to upgrade + """ + pass + + def build_upgrade_stack(self) -> List[int]: + """Builds ordered iterable of all application unit.ids to upgrade in. + + Called by leader unit during :meth:`_on_pre_upgrade_check_action`. + + Returns: + Iterable of integer unit.ids, LIFO ordered in upgrade order + i.e `[5, 2, 4, 1, 3]`, unit `3` upgrades first, `5` upgrades last + """ + # don't raise if k8s substrate, uses default statefulset order + if self.substrate == "k8s": + return [] + + raise NotImplementedError + + @abstractmethod + def log_rollback_instructions(self) -> None: + """Sets charm state and logs out rollback instructions. + + Called by all units when `state=failed` found during :meth:`_on_upgrade_changed`. + """ + pass + + def set_unit_failed(self) -> None: + """Sets unit `state=failed` to the upgrade peer data.""" + if not self.peer_relation: + return None + + # needed to refresh the stack + # now leader pulls a fresh stack from newly updated relation data + if self.charm.unit.is_leader(): + self._upgrade_stack = None + + self.peer_relation.data[self.charm.unit].update({"state": "failed"}) + + if self.substrate == "k8s": + self.on_upgrade_changed(EventBase(self.handle)) + + def set_unit_completed(self) -> None: + """Sets unit `state=completed` to the upgrade peer data.""" + if not self.peer_relation: + return None + + # needed to refresh the stack + # now leader pulls a fresh stack from newly updated relation data + if self.charm.unit.is_leader(): + self._upgrade_stack = None + + self.peer_relation.data[self.charm.unit].update({"state": "completed"}) + + if self.substrate == "k8s": + self.on_upgrade_changed(EventBase(self.handle)) + + def _on_upgrade_created(self, event: RelationCreatedEvent) -> None: + """Handler for `upgrade-relation-created` events.""" + if not self.peer_relation: + event.defer() + return + + # setting initial idle state needed to avoid execution on upgrade-changed events + self.peer_relation.data[self.charm.unit].update({"state": "idle"}) + + if self.charm.unit.is_leader(): + logger.debug("Persisting dependencies to upgrade relation data...") + self.peer_relation.data[self.charm.app].update( + {"dependencies": json.dumps(self.dependency_model.dict())} + ) + + def _on_pre_upgrade_check_action(self, event: ActionEvent) -> None: + """Handler for `pre-upgrade-check-action` events.""" + if not self.peer_relation: + event.fail(message="Could not find upgrade relation.") + return + + if not self.charm.unit.is_leader(): + event.fail(message="Action must be ran on the Juju leader.") + return + + # checking if upgrade in progress + if self.cluster_state != "idle": + event.fail("Cannot run pre-upgrade checks, cluster already upgrading.") + return + + try: + logger.info("Running pre-upgrade-check...") + self.pre_upgrade_check() + + if self.substrate == "k8s": + logger.info("Building upgrade-stack for K8s...") + built_upgrade_stack = sorted( + [int(unit.name.split("/")[1]) for unit in self.app_units] + ) + else: + logger.info("Building upgrade-stack for VMs...") + built_upgrade_stack = self.build_upgrade_stack() + + logger.debug(f"Built upgrade stack of {built_upgrade_stack}") + + except ClusterNotReadyError as e: + logger.error(e) + event.fail(message=e.message) + return + except Exception as e: + logger.error(e) + event.fail(message="Unknown error found.") + return + + logger.info("Setting upgrade-stack to relation data...") + self.upgrade_stack = built_upgrade_stack + + def _upgrade_supported_check(self) -> None: + """Checks if previous versions can be upgraded to new versions. + + Raises: + :class:`VersionError` if upgrading to existing `version` is not supported + """ + keys = self.dependency_model.__fields__.keys() + + compatible = True + incompatibilities: List[Tuple[str, str, str, str]] = [] + for key in keys: + old_dep: DependencyModel = getattr(self.stored_dependencies, key) + new_dep: DependencyModel = getattr(self.dependency_model, key) + + if not old_dep.can_upgrade(dependency=new_dep): + compatible = False + incompatibilities.append( + (key, old_dep.version, new_dep.version, new_dep.upgrade_supported) + ) + + base_message = "Versions incompatible" + base_cause = "Upgrades only supported for specific versions" + if not compatible: + for incompat in incompatibilities: + base_message += ( + f", {incompat[0]} {incompat[1]} can not be upgraded to {incompat[2]}" + ) + base_cause += f", {incompat[0]} versions satisfying requirement {incompat[3]}" + + raise VersionError( + message=base_message, + cause=base_cause, + ) + + def _on_upgrade_charm(self, event: UpgradeCharmEvent) -> None: + """Handler for `upgrade-charm` events.""" + # defer if not all units have pre-upgraded + if not self.peer_relation: + event.defer() + return + + # if any other unit failed or if no stack (i.e pre-upgrade check), mark failed + if not self.upgrade_stack or self.cluster_state == "failed": + logger.error( + "Cluster upgrade failed. Setting failed upgrade state... {}".format( + "Ensure pre-upgrade checks are ran first" if not self.upgrade_stack else "" + ) + ) + self.set_unit_failed() + self.log_rollback_instructions() + return + + # run version checks on leader only + if self.charm.unit.is_leader(): + try: + self._upgrade_supported_check() + except VersionError as e: # not ready if not passed check + logger.error(e) + self.set_unit_failed() + return + + # all units sets state to ready + self.peer_relation.data[self.charm.unit].update( + {"state": "ready" if self.substrate == "vm" else "upgrading"} + ) + + def on_upgrade_changed(self, event: EventBase) -> None: + """Handler for `upgrade-relation-changed` events.""" + if not self.peer_relation: + return + + # if any other unit failed, mark as failed + if self.cluster_state == "failed": + logger.error("Cluster upgrade failed. Setting failed upgrade state...") + self.set_unit_failed() + self.log_rollback_instructions() + return + + # if all units completed, mark as complete + if not self.upgrade_stack: + if self.state == "completed" and self.cluster_state in ["idle", "completed"]: + logger.info("All units completed upgrade, setting idle upgrade state...") + self.peer_relation.data[self.charm.unit].update({"state": "idle"}) + return + if self.cluster_state == "idle": + logger.debug("upgrade-changed event handled before pre-checks, exiting...") + return + else: + logger.debug("Did not find upgrade-stack or completed cluster state, deferring...") + event.defer() + return + + # pop mutates the `upgrade_stack` attr + top_unit_id = self.upgrade_stack.pop() + top_unit = self.charm.model.get_unit(f"{self.charm.app.name}/{top_unit_id}") + top_state = self.peer_relation.data[top_unit].get("state") + + # if top of stack is completed, leader pops it + if self.charm.unit.is_leader() and top_state == "completed": + logger.debug(f"{top_unit} has finished upgrading, updating stack...") + + # writes the mutated attr back to rel data + self.peer_relation.data[self.charm.app].update( + {"upgrade-stack": json.dumps(self.upgrade_stack)} + ) + + # recurse on leader to ensure relation changed event not lost + # in case leader is next or the last unit to complete + self.on_upgrade_changed(event) + + # if unit top of stack, emit granted event + if self.charm.unit == top_unit: + if self.substrate == "vm" and top_state in ["ready", "upgrading"]: + logger.debug( + f"{top_unit} is next to upgrade, emitting `upgrade_granted` event and upgrading..." + ) + self.peer_relation.data[self.charm.unit].update({"state": "upgrading"}) + getattr(self.on, "upgrade_granted").emit() + if self.substrate == "k8s" and top_state == "completed": + logger.debug( + f"{top_unit} has completed the upgrade, emitting `upgrade_finished` event..." + ) + getattr(self.on, "upgrade_finished").emit() + + def _on_upgrade_granted(self, event: UpgradeGrantedEvent) -> None: + """Handler for `upgrade-granted` events.""" + # don't raise if k8s substrate, only return + if self.substrate == "k8s": + return + + raise NotImplementedError + + def _on_upgrade_finished(self, event: UpgradeFinishedEvent) -> None: + """Handler for `upgrade-finished` events.""" + # don't raise if vm substrate, only return + if self.substrate == "vm": + return + + raise NotImplementedError diff --git a/lib/charms/grafana_agent/v0/cos_agent.py b/lib/charms/grafana_agent/v0/cos_agent.py index 0acaed3619..8a6cfc91bd 100644 --- a/lib/charms/grafana_agent/v0/cos_agent.py +++ b/lib/charms/grafana_agent/v0/cos_agent.py @@ -185,13 +185,13 @@ class _MetricsEndpointDict(TypedDict): port: int except ModuleNotFoundError: - _MetricsEndpointDict = dict + _MetricsEndpointDict = Dict # pyright: ignore LIBID = "dc15fa84cef84ce58155fb84f6c6213a" LIBAPI = 0 -LIBPATCH = 3 +LIBPATCH = 5 -PYDEPS = ["cosl", "pydantic"] +PYDEPS = ["cosl", "pydantic<2"] DEFAULT_RELATION_NAME = "cos-agent" DEFAULT_PEER_RELATION_NAME = "peers" @@ -217,8 +217,12 @@ def _serialize(raw_json: Union[str, bytes]) -> "GrafanaDashboard": return GrafanaDashboard(encoded) def _deserialize(self) -> Dict: - raw = lzma.decompress(base64.b64decode(self.encode("utf-8"))).decode() - return json.loads(raw) + try: + raw = lzma.decompress(base64.b64decode(self.encode("utf-8"))).decode() + return json.loads(raw) + except json.decoder.JSONDecodeError as e: + logger.error("Invalid Dashboard format: %s", e) + return {} def __repr__(self): """Return string representation of self.""" @@ -247,7 +251,7 @@ class CosAgentProviderUnitData(pydantic.BaseModel): class CosAgentPeersUnitData(pydantic.BaseModel): - """Unit databag model for `cluster` cos-agent machine charm peer relation.""" + """Unit databag model for `peers` cos-agent machine charm peer relation.""" # We need the principal unit name and relation metadata to be able to render identifiers # (e.g. topology) on the leader side, after all the data moves into peer data (the grafana @@ -307,12 +311,11 @@ def __init__( refresh_events: List of events on which to refresh relation data. """ super().__init__(charm, relation_name) - metrics_endpoints = metrics_endpoints or [DEFAULT_METRICS_ENDPOINT] dashboard_dirs = dashboard_dirs or ["./src/grafana_dashboards"] self._charm = charm self._relation_name = relation_name - self._metrics_endpoints = metrics_endpoints + self._metrics_endpoints = metrics_endpoints or [DEFAULT_METRICS_ENDPOINT] self._metrics_rules = metrics_rules_dir self._logs_rules = logs_rules_dir self._recursive = recurse_rules_dirs @@ -339,14 +342,20 @@ def _on_refresh(self, event): # Add a guard to make sure it doesn't happen. if relation.data and self._charm.unit in relation.data: # Subordinate relations can communicate only over unit data. - data = CosAgentProviderUnitData( - metrics_alert_rules=self._metrics_alert_rules, - log_alert_rules=self._log_alert_rules, - dashboards=self._dashboards, - metrics_scrape_jobs=self._scrape_jobs, - log_slots=self._log_slots, - ) - relation.data[self._charm.unit][data.KEY] = data.json() + try: + data = CosAgentProviderUnitData( + metrics_alert_rules=self._metrics_alert_rules, + log_alert_rules=self._log_alert_rules, + dashboards=self._dashboards, + metrics_scrape_jobs=self._scrape_jobs, + log_slots=self._log_slots, + ) + relation.data[self._charm.unit][data.KEY] = data.json() + except ( + pydantic.ValidationError, + json.decoder.JSONDecodeError, + ) as e: + logger.error("Invalid relation data provided: %s", e) @property def _scrape_jobs(self) -> List[Dict]: @@ -387,16 +396,33 @@ class COSAgentDataChanged(EventBase): """Event emitted by `COSAgentRequirer` when relation data changes.""" +class COSAgentValidationError(EventBase): + """Event emitted by `COSAgentRequirer` when there is an error in the relation data.""" + + def __init__(self, handle, message: str = ""): + super().__init__(handle) + self.message = message + + def snapshot(self) -> Dict: + """Save COSAgentValidationError source information.""" + return {"message": self.message} + + def restore(self, snapshot): + """Restore COSAgentValidationError source information.""" + self.message = snapshot["message"] + + class COSAgentRequirerEvents(ObjectEvents): """`COSAgentRequirer` events.""" data_changed = EventSource(COSAgentDataChanged) + validation_error = EventSource(COSAgentValidationError) class COSAgentRequirer(Object): """Integration endpoint wrapper for the Requirer side of the cos_agent interface.""" - on = COSAgentRequirerEvents() + on = COSAgentRequirerEvents() # pyright: ignore def __init__( self, @@ -426,7 +452,7 @@ def __init__( ) # TODO: do we need this? self.framework.observe(events.relation_changed, self._on_relation_data_changed) for event in self._refresh_events: - self.framework.observe(event, self.trigger_refresh) + self.framework.observe(event, self.trigger_refresh) # pyright: ignore # Peer relation events # A peer relation is needed as it is the only mechanism for exchanging data across @@ -450,7 +476,7 @@ def _on_peer_relation_changed(self, _): # Peer data is used for forwarding data from principal units to the grafana agent # subordinate leader, for updating the app data of the outgoing o11y relations. if self._charm.unit.is_leader(): - self.on.data_changed.emit() + self.on.data_changed.emit() # pyright: ignore def _on_relation_data_changed(self, event: RelationChangedEvent): # Peer data is the only means of communication between subordinate units. @@ -474,7 +500,9 @@ def _on_relation_data_changed(self, event: RelationChangedEvent): if not (raw := cos_agent_relation.data[principal_unit].get(CosAgentProviderUnitData.KEY)): return - provider_data = CosAgentProviderUnitData(**json.loads(raw)) + + if not (provider_data := self._validated_provider_data(raw)): + return # Copy data from the principal relation to the peer relation, so the leader could # follow up. @@ -492,12 +520,19 @@ def _on_relation_data_changed(self, event: RelationChangedEvent): # We can't easily tell if the data that was changed is limited to only the data # that goes into peer relation (in which case, if this is not a leader unit, we wouldn't # need to emit `on.data_changed`), so we're emitting `on.data_changed` either way. - self.on.data_changed.emit() + self.on.data_changed.emit() # pyright: ignore + + def _validated_provider_data(self, raw) -> Optional[CosAgentProviderUnitData]: + try: + return CosAgentProviderUnitData(**json.loads(raw)) + except (pydantic.ValidationError, json.decoder.JSONDecodeError) as e: + self.on.validation_error.emit(message=str(e)) # pyright: ignore + return None def trigger_refresh(self, _): """Trigger a refresh of relation data.""" # FIXME: Figure out what we should do here - self.on.data_changed.emit() + self.on.data_changed.emit() # pyright: ignore @property def _principal_unit(self) -> Optional[Unit]: @@ -529,17 +564,24 @@ def _principal_unit_data(self) -> Optional[CosAgentProviderUnitData]: Relies on the fact that, for subordinate relations, the only remote unit visible to *this unit* is the principal unit that this unit is attached to. """ - if relations := self._principal_relations: - # Technically it's a list, but for subordinates there can only be one relation - principal_relation = next(iter(relations)) - if units := principal_relation.units: - # Technically it's a list, but for subordinates there can only be one - unit = next(iter(units)) - raw = principal_relation.data[unit].get(CosAgentProviderUnitData.KEY) - if raw: - return CosAgentProviderUnitData(**json.loads(raw)) + if not (relations := self._principal_relations): + return None - return None + # Technically it's a list, but for subordinates there can only be one relation + principal_relation = next(iter(relations)) + + if not (units := principal_relation.units): + return None + + # Technically it's a list, but for subordinates there can only be one + unit = next(iter(units)) + if not (raw := principal_relation.data[unit].get(CosAgentProviderUnitData.KEY)): + return None + + if not (provider_data := self._validated_provider_data(raw)): + return None + + return provider_data def _gather_peer_data(self) -> List[CosAgentPeersUnitData]: """Collect data from the peers. @@ -578,7 +620,7 @@ def metrics_alerts(self) -> Dict[str, Any]: alert_rules = {} seen_apps: List[str] = [] - for data in self._gather_peer_data(): # type: CosAgentPeersUnitData + for data in self._gather_peer_data(): if rules := data.metrics_alert_rules: app_name = data.app_name if app_name in seen_apps: @@ -649,7 +691,7 @@ def logs_alerts(self) -> Dict[str, Any]: alert_rules = {} seen_apps: List[str] = [] - for data in self._gather_peer_data(): # type: CosAgentPeersUnitData + for data in self._gather_peer_data(): if rules := data.log_alert_rules: # This is only used for naming the file, so be as specific as we can be app_name = data.app_name @@ -678,10 +720,10 @@ def dashboards(self) -> List[Dict[str, str]]: Dashboards are assumed not to vary across units of the same primary. """ - dashboards: List[Dict[str, str]] = [] + dashboards: List[Dict[str, Any]] = [] seen_apps: List[str] = [] - for data in self._gather_peer_data(): # type: CosAgentPeersUnitData + for data in self._gather_peer_data(): app_name = data.app_name if app_name in seen_apps: continue # dedup! diff --git a/metadata.yaml b/metadata.yaml index 026e936e96..25eae19639 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -20,6 +20,8 @@ peers: interface: postgresql_peers restart: interface: rolling_op + upgrade: + interface: upgrade provides: database: diff --git a/src/charm.py b/src/charm.py index d463d4e1c6..b99428dec4 100755 --- a/src/charm.py +++ b/src/charm.py @@ -52,6 +52,7 @@ ) from constants import ( BACKUP_USER, + DEPS, METRICS_PORT, MONITORING_PASSWORD_KEY, MONITORING_SNAP_SERVICE, @@ -71,6 +72,7 @@ ) from relations.db import EXTENSIONS_BLOCKING_MESSAGE, DbProvides from relations.postgresql_provider import PostgreSQLProvider +from upgrade import PostgreSQLDependencyModel, PostgreSQLUpgrade from utils import new_password logger = logging.getLogger(__name__) @@ -103,6 +105,8 @@ def __init__(self, *args): self._member_name = self.unit.name.replace("/", "-") self._storage_path = self.meta.storages["pgdata"].location + model = PostgreSQLDependencyModel(**DEPS) + self.upgrade = PostgreSQLUpgrade(self, model) self.postgresql_client_relation = PostgreSQLProvider(self) self.legacy_db_relation = DbProvides(self, admin=False) self.legacy_db_admin_relation = DbProvides(self, admin=True) @@ -1070,18 +1074,20 @@ def _replication_password(self) -> str: """ return self.get_secret("app", REPLICATION_PASSWORD_KEY) - def _install_snap_packages(self, packages: List[str]) -> None: + def _install_snap_packages(self, packages: List[str], refresh: bool = False) -> None: """Installs package(s) to container. Args: packages: list of packages to install. + refresh: whether to refresh the snap if it's + already present. """ for snap_name, snap_version in packages: try: snap_cache = snap.SnapCache() snap_package = snap_cache[snap_name] - if not snap_package.present: + if not snap_package.present or refresh: if snap_version.get("revision"): snap_package.ensure( snap.SnapState.Latest, revision=snap_version["revision"] diff --git a/src/cluster.py b/src/cluster.py index 4229e1a5c3..c75051f110 100644 --- a/src/cluster.py +++ b/src/cluster.py @@ -7,7 +7,7 @@ import os import pwd import subprocess -from typing import Dict, Optional, Set +from typing import Dict, List, Optional, Set import requests from charms.operator_libs_linux.v2 import snap @@ -250,6 +250,19 @@ def get_primary(self, unit_name_pattern=False) -> str: primary = "/".join(primary.rsplit("-", 1)) return primary + def get_sync_standby_names(self) -> List[str]: + """Get the list of sync standby unit names.""" + sync_standbys = [] + # Request info from cluster endpoint (which returns all members of the cluster). + for attempt in Retrying(stop=stop_after_attempt(2 * len(self.peers_ips) + 1)): + with attempt: + url = self._get_alternative_patroni_url(attempt) + r = requests.get(f"{url}/cluster", verify=self.verify) + for member in r.json()["members"]: + if member["role"] == "sync_standby": + sync_standbys.append("/".join(member["name"].rsplit("-", 1))) + return sync_standbys + def _get_alternative_patroni_url(self, attempt: AttemptManager) -> str: """Get an alternative REST API URL from another member each time. diff --git a/src/constants.py b/src/constants.py index 378300005f..93f4be9534 100644 --- a/src/constants.py +++ b/src/constants.py @@ -29,6 +29,15 @@ # List of system usernames needed for correct work of the charm/workload. SYSTEM_USERS = [BACKUP_USER, REPLICATION_USER, REWIND_USER, USER, MONITORING_USER] +DEPS = { + "charm": { + "dependencies": {"pgbouncer": ">0"}, + "name": "postgresql", + "upgrade_supported": ">0", + "version": "1", + } +} + # Snap constants. PGBACKREST_EXECUTABLE = "charmed-postgresql.pgbackrest" POSTGRESQL_SNAP_NAME = "charmed-postgresql" diff --git a/src/upgrade.py b/src/upgrade.py new file mode 100644 index 0000000000..8bfae8b20e --- /dev/null +++ b/src/upgrade.py @@ -0,0 +1,120 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Upgrades implementation.""" +import logging +from typing import List + +from charms.data_platform_libs.v0.upgrade import ( + ClusterNotReadyError, + DataUpgrade, + DependencyModel, + UpgradeGrantedEvent, +) +from ops.model import ActiveStatus, MaintenanceStatus, WaitingStatus +from pydantic import BaseModel +from tenacity import RetryError, Retrying, stop_after_attempt, wait_exponential +from typing_extensions import override + +from constants import SNAP_PACKAGES + +logger = logging.getLogger(__name__) + + +class PostgreSQLDependencyModel(BaseModel): + """PostgreSQL dependencies model.""" + + charm: DependencyModel + + +class PostgreSQLUpgrade(DataUpgrade): + """PostgreSQL upgrade class.""" + + @override + def build_upgrade_stack(self) -> List[int]: + """Builds ordered iterable of all application unit.ids to upgrade in. + + Called by leader unit during :meth:`_on_pre_upgrade_check_action`. + + Returns: + Iterable of integer unit.ids, LIFO ordered in upgrade order + i.e `[5, 2, 4, 1, 3]`, unit `3` upgrades first, `5` upgrades last + """ + primary_unit_id = self.charm._patroni.get_primary(unit_name_pattern=True).split("/")[1] + sync_standby_ids = [ + unit.split("/")[1] for unit in self.charm._patroni.get_sync_standby_names() + ] + unit_ids = [self.charm.unit.name.split("/")[1]] + [ + unit.name.split("/")[1] for unit in self.charm._peers.units + ] + upgrade_stack = sorted( + unit_ids, + key=lambda x: 0 if x == primary_unit_id else 1 if x in sync_standby_ids else 2, + ) + logger.error(f"upgrade_stack: {upgrade_stack}") + return upgrade_stack + + @override + def log_rollback_instructions(self) -> None: + """Log rollback instructions.""" + logger.info("Run `juju refresh --revision postgresql` to rollback") + + @override + def _on_upgrade_granted(self, event: UpgradeGrantedEvent) -> None: + # Refresh the charmed PostgreSQL snap and restart the database. + logger.error("refreshing the snap") + self.charm.unit.status = MaintenanceStatus("refreshing the snap") + self.charm._install_snap_packages(packages=SNAP_PACKAGES, refresh=True) + + if not self.charm._patroni.start_patroni(): + logger.error("failed to start the database") + self.set_unit_failed() + return + + self.charm._setup_exporter() + self.charm.backup.start_stop_pgbackrest_service() + + # Wait until the database initialise. + self.charm.unit.status = WaitingStatus("waiting for database initialisation") + try: + for attempt in Retrying( + stop=stop_after_attempt(10), wait=wait_exponential(multiplier=1, min=2, max=30) + ): + with attempt: + if self.charm._patroni.member_started: + self.charm.unit.status = ActiveStatus() + else: + raise Exception() + except RetryError: + logger.error("Defer on_upgrade_granted: member not ready yet") + event.defer() + return + + try: + self.pre_upgrade_check() + self.set_unit_completed() + + # ensures leader gets its own relation-changed when it upgrades + if self.charm.unit.is_leader(): + self.on_upgrade_changed(event) + + except ClusterNotReadyError as e: + logger.error(e.cause) + self.set_unit_failed() + + @override + def pre_upgrade_check(self) -> None: + """Runs necessary checks validating the cluster is in a healthy state to upgrade. + + Called by all units during :meth:`_on_pre_upgrade_check_action`. + + Raises: + :class:`ClusterNotReadyError`: if cluster is not ready to upgrade + """ + if not self.charm.is_cluster_initialised: + message = "cluster has not initialised yet" + raise ClusterNotReadyError(message, message) + + # check for backups running. + + # check for tools in relation, like pgbouncer, being upgraded first? diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 4e90a1321b..89b4fe2d57 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -9,7 +9,9 @@ import boto3 import pytest from pytest_operator.plugin import OpsTest +from tenacity import Retrying, stop_after_delay, wait_fixed +from tests.integration.ha_tests.conftest import APPLICATION_NAME from tests.integration.helpers import construct_endpoint AWS = "AWS" @@ -60,6 +62,22 @@ async def cloud_configs(ops_test: OpsTest) -> None: bucket_object.delete() +@pytest.fixture() +async def continuous_writes(ops_test: OpsTest) -> None: + """Deploy the charm that makes continuous writes to PostgreSQL.""" + yield + # Clear the written data at the end. + for attempt in Retrying(stop=stop_after_delay(60 * 5), wait=wait_fixed(3), reraise=True): + with attempt: + action = ( + await ops_test.model.applications[APPLICATION_NAME] + .units[0] + .run_action("clear-continuous-writes") + ) + await action.wait() + assert action.results["result"] == "True", "Unable to clear up continuous_writes table" + + @pytest.fixture(scope="module") def ops_test(ops_test: OpsTest) -> OpsTest: if os.environ.get("CI") == "true": diff --git a/tests/integration/ha_tests/application-charm/charmcraft.yaml b/tests/integration/ha_tests/application-charm/charmcraft.yaml index bc8f6f00c7..e4b03c2dde 100644 --- a/tests/integration/ha_tests/application-charm/charmcraft.yaml +++ b/tests/integration/ha_tests/application-charm/charmcraft.yaml @@ -13,3 +13,4 @@ parts: charm: charm-binary-python-packages: - psycopg2-binary==2.9.6 # renovate + - PyYAML # https://github.com/yaml/pyyaml/issues/601 diff --git a/tests/integration/test_upgrade.py b/tests/integration/test_upgrade.py new file mode 100644 index 0000000000..5a1ea49f2f --- /dev/null +++ b/tests/integration/test_upgrade.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. +import logging + +import pytest as pytest +from pytest_operator.plugin import OpsTest + +from tests.integration.ha_tests.conftest import APPLICATION_NAME +from tests.integration.ha_tests.helpers import ( + app_name, + are_writes_increasing, + check_writes, + start_continuous_writes, +) +from tests.integration.helpers import get_primary + +logger = logging.getLogger(__name__) + + +@pytest.mark.abort_on_fail +async def test_build_and_deploy(ops_test: OpsTest) -> None: + """Build and deploy three unit of PostgreSQL.""" + wait_for_apps = False + # Check if there is a pre-existing cluster. + if not await app_name(ops_test): + wait_for_apps = True + charm = await ops_test.build_charm(".") + async with ops_test.fast_forward(): + await ops_test.model.deploy(charm, num_units=3) + # Deploy the continuous writes application charm if it wasn't already deployed. + if not await app_name(ops_test, APPLICATION_NAME): + wait_for_apps = True + async with ops_test.fast_forward(): + charm = await ops_test.build_charm("tests/integration/ha_tests/application-charm") + await ops_test.model.deploy(charm, application_name=APPLICATION_NAME) + + if wait_for_apps: + async with ops_test.fast_forward(): + await ops_test.model.wait_for_idle(status="active", timeout=1000) + + +async def test_upgrade(ops_test: OpsTest, continuous_writes) -> None: + # Start an application that continuously writes data to the database. + logger.info("starting continuous writes to the database") + app = await app_name(ops_test) + await start_continuous_writes(ops_test, app) + + # Check whether writes are increasing. + logger.info("checking whether writes are increasing") + any_unit_name = next(iter(ops_test.model.applications[app].units)).name + primary_name = await get_primary(ops_test, any_unit_name) + await are_writes_increasing(ops_test, primary_name) + + # Run the pre-upgrade-check action. + logger.info("running pre-upgrade check") + leader_unit_name = None + for unit in ops_test.model.applications[app].units: + if await unit.is_leader_from_status(): + leader_unit_name = unit.name + break + action = await ops_test.model.units.get(leader_unit_name).run_action("pre-upgrade-check") + await action.wait() + assert action.results["Code"] == "0" + + # Run juju refresh. + logger.info("refreshing the charm") + application = ops_test.model.applications[app] + charm = await ops_test.build_charm(".") + await application.refresh(path=charm) + async with ops_test.fast_forward(fast_interval="30s"): + await ops_test.model.wait_for_idle(apps=[app], status="active", idle_period=15) + + # Check whether writes are increasing. + logger.info("checking whether writes are increasing") + primary_name = await get_primary(ops_test, any_unit_name) + await are_writes_increasing(ops_test, primary_name) + + # Verify that no writes to the database were missed after stopping the writes + # (check that all the units have all the writes). + logger.info("checking whether no writes were lost") + await check_writes(ops_test) diff --git a/tox.ini b/tox.ini index 63a92519b5..a9604b8b0e 100644 --- a/tox.ini +++ b/tox.ini @@ -172,6 +172,16 @@ commands = poetry install --with integration poetry run pytest -v --tb native --log-cli-level=INFO -s {posargs} {[vars]tests_path}/integration/test_tls.py +[testenv:upgrade-integration] +description = Run upgrade integration tests +pass_env = + {[testenv]pass_env} + CI + CI_PACKED_CHARMS +commands = + poetry install --with integration + poetry run pytest -v --tb native --log-cli-level=INFO -s {posargs} {[vars]tests_path}/integration/test_upgrade.py + [testenv:integration] description = Run all integration tests pass_env = From 5f3fa35d10bf46a1d2a82fdce65200c5af4aff0f Mon Sep 17 00:00:00 2001 From: Marcelo Henrique Neppel Date: Fri, 21 Jul 2023 10:34:08 -0300 Subject: [PATCH 02/13] Updated the code with the new library --- lib/charms/data_platform_libs/v0/upgrade.py | 147 ++++++++++++++++---- 1 file changed, 121 insertions(+), 26 deletions(-) diff --git a/lib/charms/data_platform_libs/v0/upgrade.py b/lib/charms/data_platform_libs/v0/upgrade.py index 7db37383e2..90fd92f2d3 100644 --- a/lib/charms/data_platform_libs/v0/upgrade.py +++ b/lib/charms/data_platform_libs/v0/upgrade.py @@ -353,6 +353,21 @@ def __init__(self, message: str, cause: str, resolution: Optional[str] = None): super().__init__(message, cause=cause, resolution=resolution) +class KubernetesClientError(UpgradeError): + """Exception flagging that a call to Kubernetes API failed. + + For example, if the cluster fails :class:`DataUpgrade._set_rolling_update_partition` + + Args: + message: string message to be logged out + cause: short human-readable description of the cause of the error + resolution: short human-readable instructions for manual error resolution (optional) + """ + + def __init__(self, message: str, cause: str, resolution: Optional[str] = None): + super().__init__(message, cause=cause, resolution=resolution) + + class VersionError(UpgradeError): """Exception flagging that the old `version` fails to meet the new `upgrade_supported`s. @@ -401,14 +416,7 @@ class UpgradeGrantedEvent(EventBase): class UpgradeFinishedEvent(EventBase): - """Used to tell units that they finished the upgrade. - - Handlers of this event must meet the following: - - MUST trigger the upgrade in the next unit by, for example, decrementing the partition - value from the rolling update strategy - - MUST update unit `state` if the previous operation fails, calling - :class:`DataUpgrade.set_unit_failed` - """ + """Used to tell units that they finished the upgrade.""" class UpgradeEvents(CharmEvents): @@ -460,6 +468,10 @@ def __init__( self.framework.observe( getattr(self.charm.on, "pre_upgrade_check_action"), self._on_pre_upgrade_check_action ) + if self.substrate == "k8s": + self.framework.observe( + getattr(self.charm.on, "resume_upgrade_action"), self._on_resume_upgrade_action + ) @property def peer_relation(self) -> Optional[Relation]: @@ -598,9 +610,6 @@ def set_unit_failed(self) -> None: self.peer_relation.data[self.charm.unit].update({"state": "failed"}) - if self.substrate == "k8s": - self.on_upgrade_changed(EventBase(self.handle)) - def set_unit_completed(self) -> None: """Sets unit `state=completed` to the upgrade peer data.""" if not self.peer_relation: @@ -613,8 +622,12 @@ def set_unit_completed(self) -> None: self.peer_relation.data[self.charm.unit].update({"state": "completed"}) + # Emit upgrade_finished event to run unit's post upgrade operations. if self.substrate == "k8s": - self.on_upgrade_changed(EventBase(self.handle)) + logger.debug( + f"{self.charm.unit.name} has completed the upgrade, emitting `upgrade_finished` event..." + ) + getattr(self.on, "upgrade_finished").emit() def _on_upgrade_created(self, event: RelationCreatedEvent) -> None: """Handler for `upgrade-relation-created` events.""" @@ -673,6 +686,37 @@ def _on_pre_upgrade_check_action(self, event: ActionEvent) -> None: logger.info("Setting upgrade-stack to relation data...") self.upgrade_stack = built_upgrade_stack + def _on_resume_upgrade_action(self, event: ActionEvent) -> None: + """Handle resume upgrade action. + + Continue the upgrade by setting the partition to the next unit. + """ + if not self.peer_relation: + event.fail(message="Could not find upgrade relation.") + return + + if not self.charm.unit.is_leader(): + event.fail(message="Action must be ran on the Juju leader.") + return + + if not self.upgrade_stack: + event.fail(message="Nothing to resume, upgrade stack unset.") + return + + # Check whether this is being run after juju refresh was called + # (the size of the upgrade stack should match the number of total + # unit minus one). + if len(self.upgrade_stack) != len(self.peer_relation.units): + event.fail(message="Upgrade can be resumed only once after juju refresh is called.") + return + + try: + next_partition = self.upgrade_stack[-1] + self._set_rolling_update_partition(partition=next_partition) + event.set_results({"message": f"Upgrade will resume on unit {next_partition}"}) + except KubernetesClientError: + event.fail(message="Cannot set rolling update partition.") + def _upgrade_supported_check(self) -> None: """Checks if previous versions can be upgraded to new versions. @@ -784,18 +828,12 @@ def on_upgrade_changed(self, event: EventBase) -> None: self.on_upgrade_changed(event) # if unit top of stack, emit granted event - if self.charm.unit == top_unit: - if self.substrate == "vm" and top_state in ["ready", "upgrading"]: - logger.debug( - f"{top_unit} is next to upgrade, emitting `upgrade_granted` event and upgrading..." - ) - self.peer_relation.data[self.charm.unit].update({"state": "upgrading"}) - getattr(self.on, "upgrade_granted").emit() - if self.substrate == "k8s" and top_state == "completed": - logger.debug( - f"{top_unit} has completed the upgrade, emitting `upgrade_finished` event..." - ) - getattr(self.on, "upgrade_finished").emit() + if self.charm.unit == top_unit and top_state in ["ready", "upgrading"]: + logger.debug( + f"{top_unit.name} is next to upgrade, emitting `upgrade_granted` event and upgrading..." + ) + self.peer_relation.data[self.charm.unit].update({"state": "upgrading"}) + getattr(self.on, "upgrade_granted").emit() def _on_upgrade_granted(self, event: UpgradeGrantedEvent) -> None: """Handler for `upgrade-granted` events.""" @@ -805,9 +843,66 @@ def _on_upgrade_granted(self, event: UpgradeGrantedEvent) -> None: raise NotImplementedError - def _on_upgrade_finished(self, event: UpgradeFinishedEvent) -> None: + def _on_upgrade_finished(self, _) -> None: """Handler for `upgrade-finished` events.""" - # don't raise if vm substrate, only return + if self.substrate == "vm" or not self.peer_relation: + return + + # Emit the upgrade relation changed event in the leader to update the upgrade_stack. + if self.charm.unit.is_leader(): + self.charm.on[self.relation_name].relation_changed.emit( + self.model.get_relation(self.relation_name) + ) + + # This hook shouldn't run for the last unit (the first that is upgraded). For that unit it + # should be done through an action after the upgrade success on that unit is double-checked. + unit_number = int(self.charm.unit.name.split("/")[1]) + if unit_number == len(self.peer_relation.units): + logger.info( + f"{self.charm.unit.name} unit upgraded. Evaluate and run `resume-upgrade` action to continue upgrade" + ) + return + + # Also, the hook shouldn't run for the first unit (the last that is upgraded). + if unit_number == 0: + logger.info(f"{self.charm.unit.name} unit upgraded. Upgrade is complete") + return + + try: + # Use the unit number instead of the upgrade stack to avoid race conditions + # (i.e. the leader updates the upgrade stack after this hook runs). + next_partition = unit_number - 1 + logger.debug(f"Set rolling update partition to unit {next_partition}") + self._set_rolling_update_partition(partition=next_partition) + except KubernetesClientError: + logger.exception("Cannot set rolling update partition") + self.set_unit_failed() + self.log_rollback_instructions() + + def _set_rolling_update_partition(self, partition: int) -> None: + """Patch the StatefulSet's `spec.updateStrategy.rollingUpdate.partition`. + + Args: + partition: partition to set. + + K8s only. It should decrement the rolling update strategy partition by using a code + like the following: + + from lightkube.core.client import Client + from lightkube.core.exceptions import ApiError + from lightkube.resources.apps_v1 import StatefulSet + + try: + patch = {"spec": {"updateStrategy": {"rollingUpdate": {"partition": partition}}}} + Client().patch(StatefulSet, name=self.charm.model.app.name, namespace=self.charm.model.name, obj=patch) + logger.debug(f"Kubernetes StatefulSet partition set to {partition}") + except ApiError as e: + if e.status.code == 403: + cause = "`juju trust` needed" + else: + cause = str(e) + raise KubernetesClientError("Kubernetes StatefulSet patch failed", cause) + """ if self.substrate == "vm": return From c06199f79324d2819506c71b99555bac87c73089 Mon Sep 17 00:00:00 2001 From: Marcelo Henrique Neppel Date: Tue, 25 Jul 2023 16:42:38 -0300 Subject: [PATCH 03/13] Improved code and added unit tests --- poetry.lock | 573 +++++++----------- pyproject.toml | 2 +- requirements.txt | 324 +--------- src/backups.py | 4 + src/charm.py | 14 +- src/cluster.py | 21 + src/constants.py | 9 - src/dependency.json | 8 + src/upgrade.py | 95 +-- templates/patroni.yml.j2 | 4 + .../application-charm/charmcraft.yaml | 1 - .../application-charm/requirements.txt | 54 +- .../application-charm/requirements.txt | 50 +- tests/integration/test_backups.py | 2 +- tests/unit/test_backups.py | 11 + tests/unit/test_charm.py | 21 + tests/unit/test_cluster.py | 18 + tests/unit/test_upgrade.py | 146 +++++ 18 files changed, 570 insertions(+), 787 deletions(-) create mode 100644 src/dependency.json create mode 100644 tests/unit/test_upgrade.py diff --git a/poetry.lock b/poetry.lock index ac57a21a7b..d71d182461 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "appnope" version = "0.1.3" description = "Disable App Nap on macOS >= 10.9" -category = "dev" optional = false python-versions = "*" files = [ @@ -16,7 +15,6 @@ files = [ name = "asttokens" version = "2.2.1" description = "Annotate AST trees with source code positions" -category = "dev" optional = false python-versions = "*" files = [ @@ -34,7 +32,6 @@ test = ["astroid", "pytest"] name = "attrs" version = "23.1.0" description = "Classes Without Boilerplate" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -53,7 +50,6 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte name = "backcall" version = "0.2.0" description = "Specifications for callback functions passed in to an API" -category = "dev" optional = false python-versions = "*" files = [ @@ -65,7 +61,6 @@ files = [ name = "bcrypt" version = "4.0.1" description = "Modern password hashing for your software and your servers" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -100,7 +95,6 @@ typecheck = ["mypy"] name = "black" version = "23.7.0" description = "The uncompromising code formatter." -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -146,7 +140,6 @@ uvloop = ["uvloop (>=0.15.2)"] name = "boto3" version = "1.28.5" description = "The AWS SDK for Python" -category = "main" optional = false python-versions = ">= 3.7" files = [ @@ -164,14 +157,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.31.5" +version = "1.31.10" description = "Low-level, data-driven core of boto 3." -category = "main" optional = false python-versions = ">= 3.7" files = [ - {file = "botocore-1.31.5-py3-none-any.whl", hash = "sha256:8aec97512587a5475036a982785e406c52efd260457b809846985f849c3d7cf3"}, - {file = "botocore-1.31.5.tar.gz", hash = "sha256:b35114dae9c451895a11fef13d76881e2bb5428e5de8a702cc8589a28fb34c7a"}, + {file = "botocore-1.31.10-py3-none-any.whl", hash = "sha256:a3bfd3627a490faedf37d79373d6957936d7720888ca85466e0471cb921e4557"}, + {file = "botocore-1.31.10.tar.gz", hash = "sha256:736a9412f405d6985570c4a87b533c2396dd8d4042d8c7a0ca14e73d4f1bcf9d"}, ] [package.dependencies] @@ -180,37 +172,34 @@ python-dateutil = ">=2.1,<3.0.0" urllib3 = ">=1.25.4,<1.27" [package.extras] -crt = ["awscrt (==0.16.9)"] +crt = ["awscrt (==0.16.26)"] [[package]] name = "cachetools" -version = "5.3.0" +version = "5.3.1" description = "Extensible memoizing collections and decorators" -category = "dev" optional = false -python-versions = "~=3.7" +python-versions = ">=3.7" files = [ - {file = "cachetools-5.3.0-py3-none-any.whl", hash = "sha256:429e1a1e845c008ea6c85aa35d4b98b65d6a9763eeef3e37e92728a12d1de9d4"}, - {file = "cachetools-5.3.0.tar.gz", hash = "sha256:13dfddc7b8df938c21a940dfa6557ce6e94a2f1cdfa58eb90c805721d58f2c14"}, + {file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"}, + {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"}, ] [[package]] name = "certifi" -version = "2023.5.7" +version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, - {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, ] [[package]] name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." -category = "main" optional = false python-versions = "*" files = [ @@ -285,99 +274,97 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "3.1.0" +version = "3.2.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, - {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, + {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, + {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, ] [[package]] name = "click" -version = "8.1.3" +version = "8.1.6" description = "Composable command line interface toolkit" -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, + {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, + {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, ] [package.dependencies] @@ -387,7 +374,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "codespell" version = "2.2.5" description = "Codespell" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -405,7 +391,6 @@ types = ["chardet (>=5.1.0)", "mypy", "pytest", "pytest-cov", "pytest-dependency name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -417,7 +402,6 @@ files = [ name = "cosl" version = "0.0.5" description = "Utils for COS Lite charms" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -434,7 +418,6 @@ typing-extensions = "*" name = "coverage" version = "7.2.7" description = "Code coverage measurement for Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -510,7 +493,6 @@ toml = ["tomli"] name = "cryptography" version = "41.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -556,7 +538,6 @@ test-randomorder = ["pytest-randomly"] name = "decorator" version = "5.1.1" description = "Decorators for Humans" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -566,14 +547,13 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.1.1" +version = "1.1.2" description = "Backport of PEP 654 (exception groups)" -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"}, - {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"}, + {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, + {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, ] [package.extras] @@ -583,7 +563,6 @@ test = ["pytest (>=6)"] name = "executing" version = "1.2.0" description = "Get the currently executing AST node of a frame, and other information" -category = "dev" optional = false python-versions = "*" files = [ @@ -598,7 +577,6 @@ tests = ["asttokens", "littleutils", "pytest", "rich"] name = "flake8" version = "6.0.0" description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" optional = false python-versions = ">=3.8.1" files = [ @@ -615,7 +593,6 @@ pyflakes = ">=3.0.0,<3.1.0" name = "flake8-builtins" version = "2.1.0" description = "Check for python builtins being used as variables or parameters." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -633,7 +610,6 @@ test = ["pytest"] name = "flake8-copyright" version = "0.2.4" description = "Adds copyright checks to flake8" -category = "dev" optional = false python-versions = "*" files = [ @@ -648,7 +624,6 @@ setuptools = "*" name = "flake8-docstrings" version = "1.7.0" description = "Extension for flake8 which uses pydocstyle to check docstrings" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -662,35 +637,33 @@ pydocstyle = ">=2.1" [[package]] name = "google-auth" -version = "2.18.0" +version = "2.22.0" description = "Google Authentication Library" -category = "dev" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" +python-versions = ">=3.6" files = [ - {file = "google-auth-2.18.0.tar.gz", hash = "sha256:c66b488a8b005b23ccb97b1198b6cece516c91869091ac5b7c267422db2733c7"}, - {file = "google_auth-2.18.0-py2.py3-none-any.whl", hash = "sha256:ef3f3a67fa54d421a1c155864570f9a8de9179cedc937bda496b7a8ca338e936"}, + {file = "google-auth-2.22.0.tar.gz", hash = "sha256:164cba9af4e6e4e40c3a4f90a1a6c12ee56f14c0b4868d1ca91b32826ab334ce"}, + {file = "google_auth-2.22.0-py2.py3-none-any.whl", hash = "sha256:d61d1b40897407b574da67da1a833bdc10d5a11642566e506565d1b1a46ba873"}, ] [package.dependencies] cachetools = ">=2.0.0,<6.0" pyasn1-modules = ">=0.2.1" -rsa = {version = ">=3.1.4,<5", markers = "python_version >= \"3.6\""} +rsa = ">=3.1.4,<5" six = ">=1.9.0" urllib3 = "<2.0" [package.extras] -aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)", "requests (>=2.20.0,<3.0.0dev)"] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] enterprise-cert = ["cryptography (==36.0.2)", "pyopenssl (==22.0.0)"] pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] reauth = ["pyu2f (>=0.1.5)"] -requests = ["requests (>=2.20.0,<3.0.0dev)"] +requests = ["requests (>=2.20.0,<3.0.0.dev0)"] [[package]] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -702,7 +675,6 @@ files = [ name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -714,7 +686,6 @@ files = [ name = "ipdb" version = "0.13.13" description = "IPython-enabled pdb" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -729,14 +700,13 @@ tomli = {version = "*", markers = "python_version > \"3.6\" and python_version < [[package]] name = "ipython" -version = "8.13.2" +version = "8.14.0" description = "IPython: Productive Interactive Computing" -category = "dev" optional = false python-versions = ">=3.9" files = [ - {file = "ipython-8.13.2-py3-none-any.whl", hash = "sha256:ffca270240fbd21b06b2974e14a86494d6d29290184e788275f55e0b55914926"}, - {file = "ipython-8.13.2.tar.gz", hash = "sha256:7dff3fad32b97f6488e02f87b970f309d082f758d7b7fc252e3b19ee0e432dbb"}, + {file = "ipython-8.14.0-py3-none-any.whl", hash = "sha256:248aca623f5c99a6635bc3857677b7320b9b8039f99f070ee0d20a5ca5a8e6bf"}, + {file = "ipython-8.14.0.tar.gz", hash = "sha256:1d197b907b6ba441b692c48cf2a3a2de280dc0ac91a3405b39349a50272ca0a1"}, ] [package.dependencies] @@ -770,7 +740,6 @@ test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pa name = "isort" version = "5.12.0" description = "A Python utility / library to sort Python imports." -category = "dev" optional = false python-versions = ">=3.8.0" files = [ @@ -788,7 +757,6 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"] name = "jedi" version = "0.18.2" description = "An autocompletion tool for Python that can be used for text editors." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -808,7 +776,6 @@ testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -826,7 +793,6 @@ i18n = ["Babel (>=2.7)"] name = "jmespath" version = "1.0.1" description = "JSON Matching Expressions" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -838,7 +804,6 @@ files = [ name = "jsonschema" version = "4.18.4" description = "An implementation of JSON Schema validation for Python" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -860,7 +825,6 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- name = "jsonschema-specifications" version = "2023.7.1" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -873,13 +837,12 @@ referencing = ">=0.28.0" [[package]] name = "juju" -version = "2.9.43.0" +version = "2.9.44.0" description = "Python library for Juju" -category = "dev" optional = false python-versions = "*" files = [ - {file = "juju-2.9.43.0.tar.gz", hash = "sha256:185adb3a64bb24b167aba736477192417c13867540b9bb6470a309a204c9d23e"}, + {file = "juju-2.9.44.0.tar.gz", hash = "sha256:bc71fe0c8fd59ee00f0c3b03066682cd2273f299c36135451abb1a81289e68f9"}, ] [package.dependencies] @@ -888,7 +851,7 @@ macaroonbakery = ">=1.1,<2.0" paramiko = ">=2.4.0,<3.0.0" pyasn1 = ">=0.4.4" pyRFC3339 = ">=1.0,<2.0" -pyyaml = ">=5.1.2,<=6.0" +pyyaml = ">=5.1.2" theblues = ">=0.5.1,<1.0" toposort = ">=1.5,<2" typing_inspect = ">=0.6.0" @@ -898,7 +861,6 @@ websockets = {version = ">=9.0", markers = "python_version > \"3.9\""} name = "jujubundlelib" version = "0.5.7" description = "A python library for working with Juju bundles" -category = "dev" optional = false python-versions = "*" files = [ @@ -910,27 +872,26 @@ PyYAML = ">=3.11" [[package]] name = "kubernetes" -version = "26.1.0" +version = "27.2.0" description = "Kubernetes python client" -category = "dev" optional = false python-versions = ">=3.6" files = [ - {file = "kubernetes-26.1.0-py2.py3-none-any.whl", hash = "sha256:e3db6800abf7e36c38d2629b5cb6b74d10988ee0cba6fba45595a7cbe60c0042"}, - {file = "kubernetes-26.1.0.tar.gz", hash = "sha256:5854b0c508e8d217ca205591384ab58389abdae608576f9c9afc35a3c76a366c"}, + {file = "kubernetes-27.2.0-py2.py3-none-any.whl", hash = "sha256:0f9376329c85cf07615ed6886bf9bf21eb1cbfc05e14ec7b0f74ed8153cd2815"}, + {file = "kubernetes-27.2.0.tar.gz", hash = "sha256:d479931c6f37561dbfdf28fc5f46384b1cb8b28f9db344ed4a232ce91990825a"}, ] [package.dependencies] certifi = ">=14.05.14" google-auth = ">=1.0.1" +oauthlib = ">=3.2.2" python-dateutil = ">=2.5.3" pyyaml = ">=5.4.1" requests = "*" requests-oauthlib = "*" -setuptools = ">=21.0.0" six = ">=1.9.0" urllib3 = ">=1.24.2" -websocket-client = ">=0.32.0,<0.40.0 || >0.40.0,<0.41.0 || >=0.43.0" +websocket-client = ">=0.32.0,<0.40.0 || >0.40.0,<0.41.dev0 || >=0.43.dev0" [package.extras] adal = ["adal (>=1.0.2)"] @@ -939,7 +900,6 @@ adal = ["adal (>=1.0.2)"] name = "landscape-api-py3" version = "0.9.0" description = "Client for the Landscape API (Python 3)" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -954,7 +914,6 @@ requests = "*" name = "macaroonbakery" version = "1.3.1" description = "A Python library port for bakery, higher level operation to work with macaroons" -category = "dev" optional = false python-versions = "*" files = [ @@ -973,7 +932,6 @@ six = ">=1.11.0,<2.0" name = "mailmanclient" version = "3.3.5" description = "mailmanclient -- Python bindings for Mailman REST API" -category = "dev" optional = false python-versions = "*" files = [ @@ -990,69 +948,67 @@ testing = ["falcon (>1.4.1)", "httpx", "mailman (>=3.3.1)", "pytest", "pytest-se [[package]] name = "markupsafe" -version = "2.1.2" +version = "2.1.3" description = "Safely add untrusted strings to HTML/XML markup." -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, - {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, ] [[package]] name = "matplotlib-inline" version = "0.1.6" description = "Inline Matplotlib backend for Jupyter" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1067,7 +1023,6 @@ traitlets = "*" name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1079,7 +1034,6 @@ files = [ name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1091,7 +1045,6 @@ files = [ name = "oauthlib" version = "3.2.2" description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1108,7 +1061,6 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] name = "ops" version = "2.4.1" description = "The Python library behind great charms" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1117,14 +1069,13 @@ files = [ ] [package.dependencies] -PyYAML = ">=6.0.0,<7.0.0" -websocket-client = ">=1.0.0,<2.0.0" +PyYAML = "==6.*" +websocket-client = "==1.*" [[package]] name = "packaging" version = "23.1" description = "Core utilities for Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1136,7 +1087,6 @@ files = [ name = "paramiko" version = "2.12.0" description = "SSH2 protocol library" -category = "dev" optional = false python-versions = "*" files = [ @@ -1160,7 +1110,6 @@ invoke = ["invoke (>=1.3)"] name = "parso" version = "0.8.3" description = "A Python Parser" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1176,7 +1125,6 @@ testing = ["docopt", "pytest (<6.0.0)"] name = "pathspec" version = "0.11.1" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1188,7 +1136,6 @@ files = [ name = "pep8-naming" version = "0.13.3" description = "Check PEP-8 naming conventions, plugin for flake8" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1203,7 +1150,6 @@ flake8 = ">=5.0.0" name = "pexpect" version = "4.8.0" description = "Pexpect allows easy control of interactive console applications." -category = "dev" optional = false python-versions = "*" files = [ @@ -1218,7 +1164,6 @@ ptyprocess = ">=0.5" name = "pgconnstr" version = "1.0.1" description = "A tool for parsing and manipulating PostgreSQL libpq style connection strings and URIs" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1230,7 +1175,6 @@ files = [ name = "pickleshare" version = "0.7.5" description = "Tiny 'shelve'-like database with concurrency support" -category = "dev" optional = false python-versions = "*" files = [ @@ -1240,30 +1184,28 @@ files = [ [[package]] name = "platformdirs" -version = "3.5.1" +version = "3.9.1" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.5.1-py3-none-any.whl", hash = "sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5"}, - {file = "platformdirs-3.5.1.tar.gz", hash = "sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f"}, + {file = "platformdirs-3.9.1-py3-none-any.whl", hash = "sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f"}, + {file = "platformdirs-3.9.1.tar.gz", hash = "sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421"}, ] [package.extras] -docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.2.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" -version = "1.0.0" +version = "1.2.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, ] [package.extras] @@ -1272,14 +1214,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "prompt-toolkit" -version = "3.0.38" +version = "3.0.39" description = "Library for building powerful interactive command lines in Python" -category = "dev" optional = false python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.38-py3-none-any.whl", hash = "sha256:45ea77a2f7c60418850331366c81cf6b5b9cf4c7fd34616f733c5427e6abbb1f"}, - {file = "prompt_toolkit-3.0.38.tar.gz", hash = "sha256:23ac5d50538a9a38c8bde05fecb47d0b403ecd0662857a86f886f798563d5b9b"}, + {file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"}, + {file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"}, ] [package.dependencies] @@ -1289,7 +1230,6 @@ wcwidth = "*" name = "protobuf" version = "3.20.3" description = "Protocol Buffers" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1321,7 +1261,6 @@ files = [ name = "psycopg2" version = "2.9.6" description = "psycopg2 - Python-PostgreSQL Database Adapter" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1344,7 +1283,6 @@ files = [ name = "ptyprocess" version = "0.7.0" description = "Run a subprocess in a pseudo terminal" -category = "dev" optional = false python-versions = "*" files = [ @@ -1356,7 +1294,6 @@ files = [ name = "pure-eval" version = "0.2.2" description = "Safely evaluate AST nodes without side effects" -category = "dev" optional = false python-versions = "*" files = [ @@ -1371,7 +1308,6 @@ tests = ["pytest"] name = "pyasn1" version = "0.5.0" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ @@ -1383,7 +1319,6 @@ files = [ name = "pyasn1-modules" version = "0.3.0" description = "A collection of ASN.1-based protocols modules" -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ @@ -1398,7 +1333,6 @@ pyasn1 = ">=0.4.6,<0.6.0" name = "pycodestyle" version = "2.10.0" description = "Python style guide checker" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1410,7 +1344,6 @@ files = [ name = "pycparser" version = "2.21" description = "C parser in Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1422,7 +1355,6 @@ files = [ name = "pydantic" version = "1.10.11" description = "Data validation and settings management using python type hints" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1475,7 +1407,6 @@ email = ["email-validator (>=1.0.3)"] name = "pydocstyle" version = "6.3.0" description = "Python docstring style checker" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1493,7 +1424,6 @@ toml = ["tomli (>=1.2.3)"] name = "pyflakes" version = "3.0.1" description = "passive checker of Python programs" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1505,7 +1435,6 @@ files = [ name = "pygments" version = "2.15.1" description = "Pygments is a syntax highlighting package written in Python." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1520,7 +1449,6 @@ plugins = ["importlib-metadata"] name = "pymacaroons" version = "0.13.0" description = "Macaroon library for Python" -category = "dev" optional = false python-versions = "*" files = [ @@ -1536,7 +1464,6 @@ six = ">=1.8.0" name = "pynacl" version = "1.5.0" description = "Python binding to the Networking and Cryptography (NaCl) library" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1563,7 +1490,6 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] name = "pyopenssl" version = "23.2.0" description = "Python wrapper module around the OpenSSL library" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1582,7 +1508,6 @@ test = ["flaky", "pretend", "pytest (>=3.0.1)"] name = "pyproject-flake8" version = "6.0.0.post1" description = "pyproject-flake8 (`pflake8`), a monkey patching wrapper to connect flake8 with pyproject.toml configuration" -category = "dev" optional = false python-versions = ">=3.8.1" files = [ @@ -1598,7 +1523,6 @@ tomli = {version = "*", markers = "python_version < \"3.11\""} name = "pyrfc3339" version = "1.1" description = "Generate and parse RFC 3339 timestamps" -category = "dev" optional = false python-versions = "*" files = [ @@ -1613,7 +1537,6 @@ pytz = "*" name = "pytest" version = "7.4.0" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1636,7 +1559,6 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "pytest-asyncio" version = "0.21.1" description = "Pytest support for asyncio" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1655,7 +1577,6 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy name = "pytest-operator" version = "0.28.0" description = "Fixtures for Operators" -category = "dev" optional = false python-versions = "*" files = [ @@ -1675,7 +1596,6 @@ pyyaml = "*" name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -1690,7 +1610,6 @@ six = ">=1.5" name = "pytz" version = "2023.3" description = "World timezone definitions, modern and historical" -category = "dev" optional = false python-versions = "*" files = [ @@ -1700,59 +1619,57 @@ files = [ [[package]] name = "pyyaml" -version = "6.0" +version = "6.0.1" description = "YAML parser and emitter for Python" -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, - {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, - {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, - {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, - {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, - {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, - {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, - {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, + {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-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-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {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-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-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"}, ] [[package]] name = "referencing" version = "0.30.0" description = "JSON Referencing + Python" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1768,7 +1685,6 @@ rpds-py = ">=0.7.0" name = "requests" version = "2.31.0" description = "Python HTTP for Humans." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1790,7 +1706,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "requests-oauthlib" version = "1.3.1" description = "OAuthlib authentication support for Requests." -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1809,7 +1724,6 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"] name = "rpds-py" version = "0.9.2" description = "Python bindings to Rust's persistent data structures (rpds)" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1916,7 +1830,6 @@ files = [ name = "rsa" version = "4.9" description = "Pure-Python RSA implementation" -category = "dev" optional = false python-versions = ">=3.6,<4" files = [ @@ -1931,7 +1844,6 @@ pyasn1 = ">=0.1.3" name = "s3transfer" version = "0.6.1" description = "An Amazon S3 Transfer Manager" -category = "main" optional = false python-versions = ">= 3.7" files = [ @@ -1947,26 +1859,24 @@ crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] [[package]] name = "setuptools" -version = "67.7.2" +version = "68.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "setuptools-67.7.2-py3-none-any.whl", hash = "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b"}, - {file = "setuptools-67.7.2.tar.gz", hash = "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990"}, + {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, + {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1978,7 +1888,6 @@ files = [ name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -category = "dev" optional = false python-versions = "*" files = [ @@ -1990,7 +1899,6 @@ files = [ name = "stack-data" version = "0.6.2" description = "Extract data from python stack frames and tracebacks for informative displays" -category = "dev" optional = false python-versions = "*" files = [ @@ -2010,7 +1918,6 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] name = "tenacity" version = "8.2.2" description = "Retry code until it succeeds" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2025,7 +1932,6 @@ doc = ["reno", "sphinx", "tornado (>=4.5)"] name = "theblues" version = "0.5.2" description = "Python library for using the juju charm store API." -category = "dev" optional = false python-versions = "*" files = [ @@ -2041,7 +1947,6 @@ requests = ">=2.18.4" name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2053,7 +1958,6 @@ files = [ name = "toposort" version = "1.10" description = "Implements a topological sort algorithm." -category = "dev" optional = false python-versions = "*" files = [ @@ -2065,7 +1969,6 @@ files = [ name = "traitlets" version = "5.9.0" description = "Traitlets Python configuration system" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2079,26 +1982,24 @@ test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] [[package]] name = "typing-extensions" -version = "4.5.0" +version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, - {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, ] [[package]] name = "typing-inspect" -version = "0.8.0" +version = "0.9.0" description = "Runtime inspection utilities for typing module." -category = "dev" optional = false python-versions = "*" files = [ - {file = "typing_inspect-0.8.0-py3-none-any.whl", hash = "sha256:5fbf9c1e65d4fa01e701fe12a5bca6c6e08a4ffd5bc60bfac028253a447c5188"}, - {file = "typing_inspect-0.8.0.tar.gz", hash = "sha256:8b1ff0c400943b6145df8119c41c244ca8207f1f10c9c057aeed1560e4806e3d"}, + {file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"}, + {file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"}, ] [package.dependencies] @@ -2107,14 +2008,13 @@ typing-extensions = ">=3.7.4" [[package]] name = "urllib3" -version = "1.26.15" +version = "1.26.16" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"}, - {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"}, + {file = "urllib3-1.26.16-py2.py3-none-any.whl", hash = "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f"}, + {file = "urllib3-1.26.16.tar.gz", hash = "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"}, ] [package.extras] @@ -2126,7 +2026,6 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] name = "wcwidth" version = "0.2.6" description = "Measures the displayed width of unicode strings in a terminal" -category = "dev" optional = false python-versions = "*" files = [ @@ -2136,14 +2035,13 @@ files = [ [[package]] name = "websocket-client" -version = "1.5.1" +version = "1.6.1" description = "WebSocket client for Python with low level API options" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "websocket-client-1.5.1.tar.gz", hash = "sha256:3f09e6d8230892547132177f575a4e3e73cfdf06526e20cc02aa1c3b47184d40"}, - {file = "websocket_client-1.5.1-py3-none-any.whl", hash = "sha256:cdf5877568b7e83aa7cf2244ab56a3213de587bbe0ce9d8b9600fc77b455d89e"}, + {file = "websocket-client-1.6.1.tar.gz", hash = "sha256:c951af98631d24f8df89ab1019fc365f2227c0892f12fd150e935607c79dd0dd"}, + {file = "websocket_client-1.6.1-py3-none-any.whl", hash = "sha256:f1f9f2ad5291f0225a49efad77abf9e700b6fef553900623060dad6e26503b9d"}, ] [package.extras] @@ -2155,7 +2053,6 @@ test = ["websockets"] name = "websockets" version = "11.0.3" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2234,4 +2131,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10.6" -content-hash = "c3658a56159878515dddf79493ea78952be7a9348e500df8e2eb571b640c652c" +content-hash = "2b3e4da5314c68b551cb0408097375966bf0367bd8e0c92fad7e3de182d7e16b" diff --git a/pyproject.toml b/pyproject.toml index b92a7ee173..f11da9b1cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ landscape-api-py3 = "0.9.0" mailmanclient = "3.3.5" pytest = "7.4.0" pytest-operator = "0.28.0" -juju = "2.9.43.0" # Latest juju 2 +juju = "2.9.44.0" # Latest juju 2 psycopg2 = {version = "^2.9.5", extras = ["binary"]} [tool.poetry.group.ha_charm] diff --git a/requirements.txt b/requirements.txt index ea17c93ae2..2955319732 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,300 +1,24 @@ -boto3==1.28.5; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:2c76db4a1208b8d09814261fc5e530fc36b3b952ef807312495e6869fa6eaad5 \ - --hash=sha256:a5c815ab81219a606f20362c9d9c190f5c224bf33c5dc4c20501036cc4a9034f -botocore==1.31.5; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:8aec97512587a5475036a982785e406c52efd260457b809846985f849c3d7cf3 \ - --hash=sha256:b35114dae9c451895a11fef13d76881e2bb5428e5de8a702cc8589a28fb34c7a -certifi==2023.5.7 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7 \ - --hash=sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716 -cffi==1.15.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \ - --hash=sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef \ - --hash=sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104 \ - --hash=sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426 \ - --hash=sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405 \ - --hash=sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375 \ - --hash=sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a \ - --hash=sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e \ - --hash=sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc \ - --hash=sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf \ - --hash=sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185 \ - --hash=sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497 \ - --hash=sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3 \ - --hash=sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35 \ - --hash=sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c \ - --hash=sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83 \ - --hash=sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21 \ - --hash=sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca \ - --hash=sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984 \ - --hash=sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac \ - --hash=sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd \ - --hash=sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee \ - --hash=sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a \ - --hash=sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2 \ - --hash=sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192 \ - --hash=sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7 \ - --hash=sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585 \ - --hash=sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f \ - --hash=sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e \ - --hash=sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27 \ - --hash=sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b \ - --hash=sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e \ - --hash=sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e \ - --hash=sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d \ - --hash=sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c \ - --hash=sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415 \ - --hash=sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82 \ - --hash=sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02 \ - --hash=sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314 \ - --hash=sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325 \ - --hash=sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c \ - --hash=sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3 \ - --hash=sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914 \ - --hash=sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045 \ - --hash=sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d \ - --hash=sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9 \ - --hash=sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5 \ - --hash=sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2 \ - --hash=sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c \ - --hash=sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3 \ - --hash=sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2 \ - --hash=sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8 \ - --hash=sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d \ - --hash=sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d \ - --hash=sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9 \ - --hash=sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162 \ - --hash=sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76 \ - --hash=sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4 \ - --hash=sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e \ - --hash=sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9 \ - --hash=sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6 \ - --hash=sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b \ - --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \ - --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0 -charset-normalizer==3.2.0; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96 \ - --hash=sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c \ - --hash=sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710 \ - --hash=sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706 \ - --hash=sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020 \ - --hash=sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252 \ - --hash=sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad \ - --hash=sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329 \ - --hash=sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a \ - --hash=sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f \ - --hash=sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6 \ - --hash=sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4 \ - --hash=sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a \ - --hash=sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46 \ - --hash=sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2 \ - --hash=sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23 \ - --hash=sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace \ - --hash=sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd \ - --hash=sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982 \ - --hash=sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10 \ - --hash=sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2 \ - --hash=sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea \ - --hash=sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09 \ - --hash=sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5 \ - --hash=sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149 \ - --hash=sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489 \ - --hash=sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9 \ - --hash=sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80 \ - --hash=sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592 \ - --hash=sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3 \ - --hash=sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6 \ - --hash=sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed \ - --hash=sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c \ - --hash=sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200 \ - --hash=sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a \ - --hash=sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e \ - --hash=sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d \ - --hash=sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6 \ - --hash=sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623 \ - --hash=sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669 \ - --hash=sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3 \ - --hash=sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa \ - --hash=sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9 \ - --hash=sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2 \ - --hash=sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f \ - --hash=sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1 \ - --hash=sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4 \ - --hash=sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a \ - --hash=sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8 \ - --hash=sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3 \ - --hash=sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029 \ - --hash=sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f \ - --hash=sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959 \ - --hash=sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22 \ - --hash=sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7 \ - --hash=sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952 \ - --hash=sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346 \ - --hash=sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e \ - --hash=sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d \ - --hash=sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299 \ - --hash=sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd \ - --hash=sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a \ - --hash=sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3 \ - --hash=sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037 \ - --hash=sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94 \ - --hash=sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c \ - --hash=sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858 \ - --hash=sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a \ - --hash=sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449 \ - --hash=sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c \ - --hash=sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918 \ - --hash=sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1 \ - --hash=sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c \ - --hash=sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac \ - --hash=sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa -cosl==0.0.5 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:31c131d1f04c061d3fbef49a4e0a175d4cb481deeb06d0cb3c7b242e4c5416be \ - --hash=sha256:84666fde29b792299827d65a1b9b2e3c56029c769e892c8244b50ce793458894 -cryptography==41.0.2; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711 \ - --hash=sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7 \ - --hash=sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd \ - --hash=sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e \ - --hash=sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58 \ - --hash=sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0 \ - --hash=sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d \ - --hash=sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83 \ - --hash=sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831 \ - --hash=sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766 \ - --hash=sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b \ - --hash=sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c \ - --hash=sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182 \ - --hash=sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f \ - --hash=sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa \ - --hash=sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4 \ - --hash=sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a \ - --hash=sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2 \ - --hash=sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76 \ - --hash=sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5 \ - --hash=sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee \ - --hash=sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f \ - --hash=sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14 -idna==3.4 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ - --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 -jmespath==1.0.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \ - --hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe -ops==2.4.1; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:9cd1a25c6ae30dc9f9afcbe87250f5349ff9a05d5b06632ad8111700c5f0db04 \ - --hash=sha256:b5762bea03049ec150c6266833925d0bb36237b26b006ca878e74e24d162f981 -packaging==23.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \ - --hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f -pgconnstr==1.0.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:0656129961ae879675d0842f5237db82d31ce59c7b3211b051c33e37a864826e \ - --hash=sha256:0f65830e7e3b76adf4390a8592ee52343171a17caef7436257e7bc81c44e21a7 -pycparser==2.21 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ - --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 -pydantic==1.10.11; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:008c5e266c8aada206d0627a011504e14268a62091450210eda7c07fabe6963e \ - --hash=sha256:0588788a9a85f3e5e9ebca14211a496409cb3deca5b6971ff37c556d581854e7 \ - --hash=sha256:08a6c32e1c3809fbc49debb96bf833164f3438b3696abf0fbeceb417d123e6eb \ - --hash=sha256:16928fdc9cb273c6af00d9d5045434c39afba5f42325fb990add2c241402d151 \ - --hash=sha256:174899023337b9fc685ac8adaa7b047050616136ccd30e9070627c1aaab53a13 \ - --hash=sha256:192c608ad002a748e4a0bed2ddbcd98f9b56df50a7c24d9a931a8c5dd053bd3d \ - --hash=sha256:1954f8778489a04b245a1e7b8b22a9d3ea8ef49337285693cf6959e4b757535e \ - --hash=sha256:2417de68290434461a266271fc57274a138510dca19982336639484c73a07af6 \ - --hash=sha256:265a60da42f9f27e0b1014eab8acd3e53bd0bad5c5b4884e98a55f8f596b2c19 \ - --hash=sha256:331c031ba1554b974c98679bd0780d89670d6fd6f53f5d70b10bdc9addee1713 \ - --hash=sha256:373c0840f5c2b5b1ccadd9286782852b901055998136287828731868027a724f \ - --hash=sha256:3f34739a89260dfa420aa3cbd069fbcc794b25bbe5c0a214f8fb29e363484b66 \ - --hash=sha256:41e0bb6efe86281623abbeeb0be64eab740c865388ee934cd3e6a358784aca6e \ - --hash=sha256:4400015f15c9b464c9db2d5d951b6a780102cfa5870f2c036d37c23b56f7fc1b \ - --hash=sha256:44e51ba599c3ef227e168424e220cd3e544288c57829520dc90ea9cb190c3248 \ - --hash=sha256:469adf96c8e2c2bbfa655fc7735a2a82f4c543d9fee97bd113a7fb509bf5e622 \ - --hash=sha256:5b02d24f7b2b365fed586ed73582c20f353a4c50e4be9ba2c57ab96f8091ddae \ - --hash=sha256:7522a7666157aa22b812ce14c827574ddccc94f361237ca6ea8bb0d5c38f1629 \ - --hash=sha256:787cf23e5a0cde753f2eabac1b2e73ae3844eb873fd1f5bdbff3048d8dbb7604 \ - --hash=sha256:8268a735a14c308923e8958363e3a3404f6834bb98c11f5ab43251a4e410170c \ - --hash=sha256:8dc77064471780262b6a68fe67e013298d130414d5aaf9b562c33987dbd2cf4f \ - --hash=sha256:a451ccab49971af043ec4e0d207cbc8cbe53dbf148ef9f19599024076fe9c25b \ - --hash=sha256:a6c098d4ab5e2d5b3984d3cb2527e2d6099d3de85630c8934efcfdc348a9760e \ - --hash=sha256:abade85268cc92dff86d6effcd917893130f0ff516f3d637f50dadc22ae93999 \ - --hash=sha256:bc64eab9b19cd794a380179ac0e6752335e9555d214cfcb755820333c0784cb3 \ - --hash=sha256:c3339a46bbe6013ef7bdd2844679bfe500347ac5742cd4019a88312aa58a9847 \ - --hash=sha256:d185819a7a059550ecb85d5134e7d40f2565f3dd94cfd870132c5f91a89cf58c \ - --hash=sha256:d7781f1d13b19700b7949c5a639c764a077cbbdd4322ed505b449d3ca8edcb36 \ - --hash=sha256:e297897eb4bebde985f72a46a7552a7556a3dd11e7f76acda0c1093e3dbcf216 \ - --hash=sha256:e6cbfbd010b14c8a905a7b10f9fe090068d1744d46f9e0c021db28daeb8b6de1 \ - --hash=sha256:e9738b0f2e6c70f44ee0de53f2089d6002b10c33264abee07bdb5c7f03038303 \ - --hash=sha256:e9baf78b31da2dc3d3f346ef18e58ec5f12f5aaa17ac517e2ffd026a92a87588 \ - --hash=sha256:ef55392ec4bb5721f4ded1096241e4b7151ba6d50a50a80a2526c854f42e6a2f \ - --hash=sha256:f66d479cf7eb331372c470614be6511eae96f1f120344c25f3f9bb59fb1b5528 \ - --hash=sha256:fe429898f2c9dd209bd0632a606bddc06f8bce081bbd03d1c775a45886e2c1cb \ - --hash=sha256:ff44c5e89315b15ff1f7fdaf9853770b810936d6b01a7bcecaa227d2f8fe444f -pyOpenSSL==23.2.0; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:24f0dc5227396b3e831f4c7f602b950a5e9833d292c8e4a2e06b709292806ae2 \ - --hash=sha256:276f931f55a452e7dea69c7173e984eb2a4407ce413c918aa34b55f82f9b8bac -python-dateutil==2.8.2 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ - --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 -PyYAML==6.0.1; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ - --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ - --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ - --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ - --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ - --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ - --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ - --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ - --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ - --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ - --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ - --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ - --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ - --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ - --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ - --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ - --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ - --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ - --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ - --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ - --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ - --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ - --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ - --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ - --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ - --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ - --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ - --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ - --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ - --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ - --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ - --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ - --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ - --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ - --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ - --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ - --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ - --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ - --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ - --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f -requests==2.31.0; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ - --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 -s3transfer==0.6.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346 \ - --hash=sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9 -six==1.16.0 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ - --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 -tenacity==8.2.2 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:2f277afb21b851637e8f52e6a613ff08734c347dc19ade928e519d7d2d8569b0 \ - --hash=sha256:43af037822bd0029025877f3b2d97cc4d7bb0c2991000a3d59d71517c5c969e0 -typing-extensions==4.7.1; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \ - --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2 -urllib3==1.26.16; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f \ - --hash=sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14 -websocket-client==1.6.1; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:c951af98631d24f8df89ab1019fc365f2227c0892f12fd150e935607c79dd0dd \ - --hash=sha256:f1f9f2ad5291f0225a49efad77abf9e700b6fef553900623060dad6e26503b9d +boto3==1.28.5 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +botocore==1.31.10 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +certifi==2023.7.22 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +cffi==1.15.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +charset-normalizer==3.2.0 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +cosl==0.0.5 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +cryptography==41.0.2 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +idna==3.4 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +jmespath==1.0.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +ops==2.4.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +packaging==23.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +pgconnstr==1.0.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +pycparser==2.21 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +pydantic==1.10.11 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +pyopenssl==23.2.0 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +python-dateutil==2.8.2 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +pyyaml==6.0.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +requests==2.31.0 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +s3transfer==0.6.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +six==1.16.0 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +tenacity==8.2.2 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +typing-extensions==4.7.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +urllib3==1.26.16 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +websocket-client==1.6.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" diff --git a/src/backups.py b/src/backups.py index 4aa1561998..6c04064a03 100644 --- a/src/backups.py +++ b/src/backups.py @@ -485,6 +485,9 @@ def _on_create_backup_action(self, event) -> None: self._change_connectivity_to_database(connectivity=False) self.charm.unit.status = MaintenanceStatus("creating backup") + # Set flag due to missing in progress backups on JSON output + # (reference: https://github.com/pgbackrest/pgbackrest/issues/2007) + self.charm.update_config(is_creating_backup=True) self._run_backup(event, s3_parameters, datetime_backup_requested) @@ -493,6 +496,7 @@ def _on_create_backup_action(self, event) -> None: # and update the Patroni configuration. self._change_connectivity_to_database(connectivity=True) + self.charm.update_config(is_creating_backup=False) self.charm.unit.status = ActiveStatus() def _run_backup( diff --git a/src/charm.py b/src/charm.py index b99428dec4..5e3b125fa9 100755 --- a/src/charm.py +++ b/src/charm.py @@ -52,7 +52,6 @@ ) from constants import ( BACKUP_USER, - DEPS, METRICS_PORT, MONITORING_PASSWORD_KEY, MONITORING_SNAP_SERVICE, @@ -72,7 +71,7 @@ ) from relations.db import EXTENSIONS_BLOCKING_MESSAGE, DbProvides from relations.postgresql_provider import PostgreSQLProvider -from upgrade import PostgreSQLDependencyModel, PostgreSQLUpgrade +from upgrade import PostgreSQLUpgrade, get_postgresql_dependencies_model from utils import new_password logger = logging.getLogger(__name__) @@ -105,8 +104,12 @@ def __init__(self, *args): self._member_name = self.unit.name.replace("/", "-") self._storage_path = self.meta.storages["pgdata"].location - model = PostgreSQLDependencyModel(**DEPS) - self.upgrade = PostgreSQLUpgrade(self, model) + self.upgrade = PostgreSQLUpgrade( + self, + model=get_postgresql_dependencies_model(), + relation_name="upgrade", + substrate="vm", + ) self.postgresql_client_relation = PostgreSQLProvider(self) self.legacy_db_relation = DbProvides(self, admin=False) self.legacy_db_admin_relation = DbProvides(self, admin=True) @@ -1190,13 +1193,14 @@ def _restart(self, event: RunWithLock) -> None: # Start or stop the pgBackRest TLS server service when TLS certificate change. self.backup.start_stop_pgbackrest_service() - def update_config(self) -> None: + def update_config(self, is_creating_backup: bool = False) -> None: """Updates Patroni config file based on the existence of the TLS files.""" enable_tls = all(self.tls.get_tls_files()) # Update and reload configuration based on TLS files availability. self._patroni.render_patroni_yml_file( connectivity=self.unit_peer_data.get("connectivity", "on") == "on", + is_creating_backup=is_creating_backup, enable_tls=enable_tls, backup_id=self.app_peer_data.get("restoring-backup"), stanza=self.app_peer_data.get("stanza"), diff --git a/src/cluster.py b/src/cluster.py index c75051f110..e02a5b1561 100644 --- a/src/cluster.py +++ b/src/cluster.py @@ -324,6 +324,24 @@ def get_patroni_health(self) -> Dict[str, str]: return r.json() + @property + def is_creating_backup(self) -> bool: + """Returns whether a backup is being created.""" + # Request info from cluster endpoint (which returns the list of tags from each + # cluster member; the "is_creating_backup" tag means that the member is creating + # a backup). + try: + for attempt in Retrying(stop=stop_after_delay(10), wait=wait_fixed(3)): + with attempt: + r = requests.get(f"{self._patroni_url}/cluster", verify=self.verify) + except RetryError: + return False + + return any( + "tags" in member and member["tags"].get("is_creating_backup") + for member in r.json()["members"] + ) + @property def member_started(self) -> bool: """Has the member started Patroni and PostgreSQL. @@ -412,6 +430,7 @@ def render_file(self, path: str, content: str, mode: int) -> None: def render_patroni_yml_file( self, connectivity: bool = False, + is_creating_backup: bool = False, enable_tls: bool = False, stanza: str = None, restore_stanza: Optional[str] = None, @@ -421,6 +440,7 @@ def render_patroni_yml_file( Args: connectivity: whether to allow external connections to the database. + is_creating_backup: whether this unit is creating a backup. enable_tls: whether to enable TLS. stanza: name of the stanza created by pgBackRest. restore_stanza: name of the stanza used when restoring a backup. @@ -433,6 +453,7 @@ def render_patroni_yml_file( rendered = template.render( conf_path=PATRONI_CONF_PATH, connectivity=connectivity, + is_creating_backup=is_creating_backup, log_path=PATRONI_LOGS_PATH, data_path=POSTGRESQL_DATA_PATH, enable_tls=enable_tls, diff --git a/src/constants.py b/src/constants.py index 16fe8dedc6..88d460130d 100644 --- a/src/constants.py +++ b/src/constants.py @@ -29,15 +29,6 @@ # List of system usernames needed for correct work of the charm/workload. SYSTEM_USERS = [BACKUP_USER, REPLICATION_USER, REWIND_USER, USER, MONITORING_USER] -DEPS = { - "charm": { - "dependencies": {"pgbouncer": ">0"}, - "name": "postgresql", - "upgrade_supported": ">0", - "version": "1", - } -} - # Snap constants. PGBACKREST_EXECUTABLE = "charmed-postgresql.pgbackrest" POSTGRESQL_SNAP_NAME = "charmed-postgresql" diff --git a/src/dependency.json b/src/dependency.json new file mode 100644 index 0000000000..fde65758d5 --- /dev/null +++ b/src/dependency.json @@ -0,0 +1,8 @@ +{ + "charm": { + "dependencies": {"pgbouncer": ">0"}, + "name": "postgresql", + "upgrade_supported": ">0", + "version": "1" + } +} diff --git a/src/upgrade.py b/src/upgrade.py index 8bfae8b20e..0936d59ae7 100644 --- a/src/upgrade.py +++ b/src/upgrade.py @@ -2,6 +2,7 @@ # See LICENSE file for licensing details. """Upgrades implementation.""" +import json import logging from typing import List @@ -13,7 +14,7 @@ ) from ops.model import ActiveStatus, MaintenanceStatus, WaitingStatus from pydantic import BaseModel -from tenacity import RetryError, Retrying, stop_after_attempt, wait_exponential +from tenacity import RetryError, Retrying, stop_after_attempt, wait_fixed from typing_extensions import override from constants import SNAP_PACKAGES @@ -27,9 +28,21 @@ class PostgreSQLDependencyModel(BaseModel): charm: DependencyModel +def get_postgresql_dependencies_model() -> PostgreSQLDependencyModel: + """Return the PostgreSQL dependencies model.""" + with open("src/dependency.json") as dependency_file: + _deps = json.load(dependency_file) + return PostgreSQLDependencyModel(**_deps) + + class PostgreSQLUpgrade(DataUpgrade): """PostgreSQL upgrade class.""" + def __init__(self, charm, model: BaseModel, **kwargs) -> None: + """Initialize the class.""" + super().__init__(charm, model, **kwargs) + self.charm = charm + @override def build_upgrade_stack(self) -> List[int]: """Builds ordered iterable of all application unit.ids to upgrade in. @@ -40,29 +53,33 @@ def build_upgrade_stack(self) -> List[int]: Iterable of integer unit.ids, LIFO ordered in upgrade order i.e `[5, 2, 4, 1, 3]`, unit `3` upgrades first, `5` upgrades last """ - primary_unit_id = self.charm._patroni.get_primary(unit_name_pattern=True).split("/")[1] + primary_unit_id = int( + self.charm._patroni.get_primary(unit_name_pattern=True).split("/")[1] + ) sync_standby_ids = [ - unit.split("/")[1] for unit in self.charm._patroni.get_sync_standby_names() + int(unit.split("/")[1]) for unit in self.charm._patroni.get_sync_standby_names() ] - unit_ids = [self.charm.unit.name.split("/")[1]] + [ - unit.name.split("/")[1] for unit in self.charm._peers.units + unit_ids = [int(self.charm.unit.name.split("/")[1])] + [ + int(unit.name.split("/")[1]) for unit in self.peer_relation.units ] + # Sort the upgrade stack so replicas are upgraded first, then the sync-standbys + # at the primary is the last unit to be upgraded. upgrade_stack = sorted( unit_ids, key=lambda x: 0 if x == primary_unit_id else 1 if x in sync_standby_ids else 2, ) - logger.error(f"upgrade_stack: {upgrade_stack}") return upgrade_stack @override def log_rollback_instructions(self) -> None: """Log rollback instructions.""" - logger.info("Run `juju refresh --revision postgresql` to rollback") + logger.info( + "Run `juju refresh --revision postgresql` to initiate the rollback" + ) @override def _on_upgrade_granted(self, event: UpgradeGrantedEvent) -> None: # Refresh the charmed PostgreSQL snap and restart the database. - logger.error("refreshing the snap") self.charm.unit.status = MaintenanceStatus("refreshing the snap") self.charm._install_snap_packages(packages=SNAP_PACKAGES, refresh=True) @@ -77,30 +94,31 @@ def _on_upgrade_granted(self, event: UpgradeGrantedEvent) -> None: # Wait until the database initialise. self.charm.unit.status = WaitingStatus("waiting for database initialisation") try: - for attempt in Retrying( - stop=stop_after_attempt(10), wait=wait_exponential(multiplier=1, min=2, max=30) - ): + for attempt in Retrying(stop=stop_after_attempt(6), wait=wait_fixed(10)): with attempt: - if self.charm._patroni.member_started: - self.charm.unit.status = ActiveStatus() - else: + # Check if the member hasn't started or hasn't joined the cluster yet. + if ( + not self.charm._patroni.member_started + or self.charm.unit.name.replace("/", "-") + not in self.charm._patroni.cluster_members + ): + logger.debug( + "Instance not yet back in the cluster." + f" Retry {attempt.retry_state.attempt_number}/6" + ) raise Exception() - except RetryError: - logger.error("Defer on_upgrade_granted: member not ready yet") - event.defer() - return - try: - self.pre_upgrade_check() - self.set_unit_completed() - - # ensures leader gets its own relation-changed when it upgrades - if self.charm.unit.is_leader(): - self.on_upgrade_changed(event) + self.set_unit_completed() + self.charm.unit.status = ActiveStatus() - except ClusterNotReadyError as e: - logger.error(e.cause) - self.set_unit_failed() + # Ensures leader gets its own relation-changed when it upgrades + if self.charm.unit.is_leader(): + self.on_upgrade_changed(event) + except RetryError: + logger.debug( + "Defer on_upgrade_granted: member not ready or not joined the cluster yet" + ) + event.defer() @override def pre_upgrade_check(self) -> None: @@ -111,10 +129,17 @@ def pre_upgrade_check(self) -> None: Raises: :class:`ClusterNotReadyError`: if cluster is not ready to upgrade """ - if not self.charm.is_cluster_initialised: - message = "cluster has not initialised yet" - raise ClusterNotReadyError(message, message) - - # check for backups running. - - # check for tools in relation, like pgbouncer, being upgraded first? + default_message = "Pre-upgrade check failed and cannot safely upgrade" + if not self.charm._patroni.are_all_members_ready(): + raise ClusterNotReadyError( + default_message, + "not all members are ready yet", + "wait for all units to become active/idle", + ) + + if self.charm._patroni.is_creating_backup: + raise ClusterNotReadyError( + default_message, + "a backup is being created", + "wait for the backup creation to finish before starting the upgrade", + ) diff --git a/templates/patroni.yml.j2 b/templates/patroni.yml.j2 index 135c79a4eb..b143e316e5 100644 --- a/templates/patroni.yml.j2 +++ b/templates/patroni.yml.j2 @@ -124,3 +124,7 @@ postgresql: username: {{ superuser }} password: {{ superuser_password }} use_unix_socket: true +{%- if is_creating_backup %} +tags: + is_creating_backup: {{ is_creating_backup }} +{%- endif %} diff --git a/tests/integration/ha_tests/application-charm/charmcraft.yaml b/tests/integration/ha_tests/application-charm/charmcraft.yaml index e4b03c2dde..bc8f6f00c7 100644 --- a/tests/integration/ha_tests/application-charm/charmcraft.yaml +++ b/tests/integration/ha_tests/application-charm/charmcraft.yaml @@ -13,4 +13,3 @@ parts: charm: charm-binary-python-packages: - psycopg2-binary==2.9.6 # renovate - - PyYAML # https://github.com/yaml/pyyaml/issues/601 diff --git a/tests/integration/ha_tests/application-charm/requirements.txt b/tests/integration/ha_tests/application-charm/requirements.txt index c7dc0ff40b..fb73d43ed0 100644 --- a/tests/integration/ha_tests/application-charm/requirements.txt +++ b/tests/integration/ha_tests/application-charm/requirements.txt @@ -1,50 +1,4 @@ -ops==2.4.1; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:9cd1a25c6ae30dc9f9afcbe87250f5349ff9a05d5b06632ad8111700c5f0db04 \ - --hash=sha256:b5762bea03049ec150c6266833925d0bb36237b26b006ca878e74e24d162f981 -PyYAML==6.0.1; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ - --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ - --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ - --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ - --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ - --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ - --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ - --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ - --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ - --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ - --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ - --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ - --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ - --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ - --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ - --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ - --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ - --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ - --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ - --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ - --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ - --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ - --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ - --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ - --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ - --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ - --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ - --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ - --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ - --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ - --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ - --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ - --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ - --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ - --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ - --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ - --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ - --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ - --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ - --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f -tenacity==8.2.2 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:2f277afb21b851637e8f52e6a613ff08734c347dc19ade928e519d7d2d8569b0 \ - --hash=sha256:43af037822bd0029025877f3b2d97cc4d7bb0c2991000a3d59d71517c5c969e0 -websocket-client==1.6.1; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:c951af98631d24f8df89ab1019fc365f2227c0892f12fd150e935607c79dd0dd \ - --hash=sha256:f1f9f2ad5291f0225a49efad77abf9e700b6fef553900623060dad6e26503b9d +ops==2.4.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +pyyaml==6.0.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +tenacity==8.2.2 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +websocket-client==1.6.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" diff --git a/tests/integration/new_relations/application-charm/requirements.txt b/tests/integration/new_relations/application-charm/requirements.txt index cf4a6a9d64..0a50b8100e 100644 --- a/tests/integration/new_relations/application-charm/requirements.txt +++ b/tests/integration/new_relations/application-charm/requirements.txt @@ -1,47 +1,3 @@ -ops==2.4.1; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:9cd1a25c6ae30dc9f9afcbe87250f5349ff9a05d5b06632ad8111700c5f0db04 \ - --hash=sha256:b5762bea03049ec150c6266833925d0bb36237b26b006ca878e74e24d162f981 -PyYAML==6.0.1; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ - --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ - --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ - --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ - --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ - --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ - --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ - --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ - --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ - --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ - --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ - --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ - --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ - --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ - --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ - --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ - --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ - --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ - --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ - --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ - --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ - --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ - --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ - --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ - --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ - --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ - --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ - --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ - --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ - --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ - --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ - --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ - --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ - --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ - --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ - --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ - --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ - --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ - --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ - --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f -websocket-client==1.6.1; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:c951af98631d24f8df89ab1019fc365f2227c0892f12fd150e935607c79dd0dd \ - --hash=sha256:f1f9f2ad5291f0225a49efad77abf9e700b6fef553900623060dad6e26503b9d +ops==2.4.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +pyyaml==6.0.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +websocket-client==1.6.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" diff --git a/tests/integration/test_backups.py b/tests/integration/test_backups.py index 4ff6fc4ec5..b1d05858df 100644 --- a/tests/integration/test_backups.py +++ b/tests/integration/test_backups.py @@ -299,7 +299,7 @@ async def test_invalid_config_and_recovery_after_fixing_it( # Provide valid backup configurations, with another path in the S3 bucket. logger.info("configuring S3 integrator for a valid cloud") config = cloud_configs[0][AWS].copy() - config["path"] = f"/postgresql-k8s/{uuid.uuid1()}" + config["path"] = f"/postgresql/{uuid.uuid1()}" await ops_test.model.applications[S3_INTEGRATOR_APP_NAME].set_config(config) logger.info("waiting for the database charm to become active") await ops_test.model.wait_for_idle( diff --git a/tests/unit/test_backups.py b/tests/unit/test_backups.py index 8c6c2a8e7a..ae98b1b7c4 100644 --- a/tests/unit/test_backups.py +++ b/tests/unit/test_backups.py @@ -705,6 +705,7 @@ def test_on_s3_credential_changed( _initialise_stanza.assert_called_once() _start_stop_pgbackrest_service.assert_called_once() + @patch("charm.PostgresqlOperatorCharm.update_config") @patch("charm.PostgreSQLBackups._change_connectivity_to_database") @patch("charm.PostgreSQLBackups._list_backups") @patch("charm.PostgreSQLBackups._execute_command") @@ -725,6 +726,7 @@ def test_on_create_backup_action( _execute_command, _list_backups, _change_connectivity_to_database, + _update_config, ): # Test when the unit cannot perform a backup. mock_event = MagicMock() @@ -772,6 +774,11 @@ def test_on_create_backup_action( _is_primary.return_value = True _execute_command.return_value = (1, "", "fake error") self.charm.backup._on_create_backup_action(mock_event) + update_config_calls = [ + call(is_creating_backup=True), + call(is_creating_backup=False), + ] + _update_config.assert_has_calls(update_config_calls) mock_event.fail.assert_called_once() mock_event.set_results.assert_not_called() @@ -782,6 +789,7 @@ def test_on_create_backup_action( _execute_command.side_effect = None _execute_command.return_value = (0, "fake stdout", "fake stderr") _list_backups.return_value = {"2023-01-01T09:00:00Z": self.charm.backup.stanza_name} + _update_config.reset_mock() self.charm.backup._on_create_backup_action(mock_event) _upload_content_to_s3.assert_has_calls( [ @@ -797,6 +805,7 @@ def test_on_create_backup_action( ), ] ) + _update_config.assert_has_calls(update_config_calls) mock_event.fail.assert_called_once() mock_event.set_results.assert_not_called() @@ -805,6 +814,7 @@ def test_on_create_backup_action( _upload_content_to_s3.reset_mock() _upload_content_to_s3.side_effect = None _upload_content_to_s3.return_value = True + _update_config.reset_mock() self.charm.backup._on_create_backup_action(mock_event) _upload_content_to_s3.assert_has_calls( [ @@ -821,6 +831,7 @@ def test_on_create_backup_action( ] ) _change_connectivity_to_database.assert_not_called() + _update_config.assert_has_calls(update_config_calls) mock_event.fail.assert_not_called() mock_event.set_results.assert_called_once() diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index e94bb9ce99..59bf63b73b 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -775,6 +775,25 @@ def test_install_snap_packages(self, _snap_cache): _snap_package.ensure.assert_called_once_with(snap.SnapState.Latest, revision=42) _snap_package.hold.assert_called_once_with() + # Test with refresh + _snap_cache.reset_mock() + _snap_package.reset_mock() + _snap_package.present = True + self.charm._install_snap_packages([("postgresql", {"revision": 42})], refresh=True) + _snap_cache.assert_called_once_with() + _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") + _snap_package.ensure.assert_called_once_with(snap.SnapState.Latest, revision=42) + _snap_package.hold.assert_called_once_with() + + # Test without refresh + _snap_cache.reset_mock() + _snap_package.reset_mock() + self.charm._install_snap_packages([("postgresql", {"revision": 42})]) + _snap_cache.assert_called_once_with() + _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") + _snap_package.ensure.assert_not_called() + _snap_package.hold.assert_not_called() + @patch_network_get(private_address="1.1.1.1") @patch("charm.PostgresqlOperatorCharm._on_leader_elected") def test_get_secret(self, _): @@ -888,6 +907,7 @@ def test_update_config( self.charm.update_config() _render_patroni_yml_file.assert_called_once_with( connectivity=True, + is_creating_backup=False, enable_tls=False, backup_id=None, stanza=None, @@ -909,6 +929,7 @@ def test_update_config( self.charm.update_config() _render_patroni_yml_file.assert_called_once_with( connectivity=True, + is_creating_backup=False, enable_tls=True, backup_id=None, stanza=None, diff --git a/tests/unit/test_cluster.py b/tests/unit/test_cluster.py index 1c1696401a..6cf8860d2f 100644 --- a/tests/unit/test_cluster.py +++ b/tests/unit/test_cluster.py @@ -145,6 +145,24 @@ def test_get_primary(self, _get_alternative_patroni_url, _get): primary = self.patroni.get_primary(unit_name_pattern=True) self.assertEqual(primary, "postgresql/0") + @patch("requests.get") + def test_is_creating_backup(self, _get): + # Test when one member is creating a backup. + response = _get.return_value + response.json.return_value = { + "members": [ + {"name": "postgresql-0"}, + {"name": "postgresql-1", "tags": {"is_creating_backup": True}}, + ] + } + self.assertTrue(self.patroni.is_creating_backup) + + # Test when no member is creating a backup. + response.json.return_value = { + "members": [{"name": "postgresql-0"}, {"name": "postgresql-1"}] + } + self.assertFalse(self.patroni.is_creating_backup) + @patch("cluster.stop_after_delay", return_value=tenacity.stop_after_delay(0)) @patch("cluster.wait_fixed", return_value=tenacity.wait_fixed(0)) @patch("requests.get", side_effect=mocked_requests_get) diff --git a/tests/unit/test_upgrade.py b/tests/unit/test_upgrade.py new file mode 100644 index 0000000000..f0b865a86e --- /dev/null +++ b/tests/unit/test_upgrade.py @@ -0,0 +1,146 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. +import unittest +from unittest.mock import MagicMock, PropertyMock, patch + +import tenacity +from charms.data_platform_libs.v0.upgrade import ClusterNotReadyError +from ops.testing import Harness + +from charm import PostgresqlOperatorCharm +from constants import SNAP_PACKAGES +from tests.helpers import patch_network_get + + +class TestUpgrade(unittest.TestCase): + """Test the upgrade class.""" + + def setUp(self): + """Set up the test.""" + self.harness = Harness(PostgresqlOperatorCharm) + self.harness.begin() + self.upgrade_relation_id = self.harness.add_relation("upgrade", "postgresql") + self.peer_relation_id = self.harness.add_relation("database-peers", "postgresql") + for rel_id in (self.upgrade_relation_id, self.peer_relation_id): + self.harness.add_relation_unit(rel_id, "postgresql/1") + with self.harness.hooks_disabled(): + self.harness.update_relation_data( + self.upgrade_relation_id, "postgresql/1", {"state": "idle"} + ) + self.charm = self.harness.charm + + @patch_network_get(private_address="1.1.1.1") + @patch("charm.Patroni.get_sync_standby_names") + @patch("charm.Patroni.get_primary") + def test_build_upgrade_stack(self, _get_primary, _get_sync_standby_names): + # Set some side effects to test multiple situations. + _get_primary.side_effect = ["postgresql/0", "postgresql/1"] + _get_sync_standby_names.side_effect = [["postgresql/1"], ["postgresql/2"]] + for rel_id in (self.upgrade_relation_id, self.peer_relation_id): + self.harness.add_relation_unit(rel_id, "postgresql/2") + + self.assertEqual(self.charm.upgrade.build_upgrade_stack(), [0, 1, 2]) + self.assertEqual(self.charm.upgrade.build_upgrade_stack(), [1, 2, 0]) + + @patch("charm.PostgresqlOperatorCharm.update_config") + @patch("upgrade.logger.info") + def test_log_rollback(self, mock_logging, _update_config): + self.charm.upgrade.log_rollback_instructions() + mock_logging.assert_any_call( + "Run `juju refresh --revision postgresql` to initiate the rollback" + ) + + @patch_network_get(private_address="1.1.1.1") + @patch("charms.data_platform_libs.v0.upgrade.DataUpgrade.set_unit_failed") + @patch("charms.data_platform_libs.v0.upgrade.DataUpgrade.set_unit_completed") + @patch("charm.Patroni.cluster_members", new_callable=PropertyMock) + @patch("charm.Patroni.member_started", new_callable=PropertyMock) + @patch("upgrade.wait_fixed", return_value=tenacity.wait_fixed(0)) + @patch("charm.PostgreSQLBackups.start_stop_pgbackrest_service") + @patch("charm.PostgresqlOperatorCharm._setup_exporter") + @patch("charm.Patroni.start_patroni") + @patch("charm.PostgresqlOperatorCharm._install_snap_packages") + def test_on_upgrade_granted( + self, + _install_snap_packages, + _start_patroni, + _setup_exporter, + _start_stop_pgbackrest_service, + _, + _member_started, + _cluster_members, + _set_unit_completed, + _set_unit_failed, + ): + # Test when the charm fails to start Patroni. + mock_event = MagicMock() + _start_patroni.return_value = False + self.charm.upgrade._on_upgrade_granted(mock_event) + _install_snap_packages.assert_called_once_with(packages=SNAP_PACKAGES, refresh=True) + _member_started.assert_not_called() + mock_event.defer.assert_not_called() + _set_unit_completed.assert_not_called() + _set_unit_failed.assert_called_once() + + # Test when the member hasn't started yet. + _set_unit_failed.reset_mock() + _start_patroni.return_value = True + _member_started.return_value = False + self.charm.upgrade._on_upgrade_granted(mock_event) + self.assertEqual(_member_started.call_count, 6) + _cluster_members.assert_not_called() + mock_event.defer.assert_called_once() + _set_unit_completed.assert_not_called() + _set_unit_failed.assert_not_called() + + # Test when the member has already started but not joined the cluster yet. + _member_started.reset_mock() + mock_event.defer.reset_mock() + _member_started.return_value = True + _cluster_members.return_value = ["postgresql-1"] + self.charm.upgrade._on_upgrade_granted(mock_event) + self.assertEqual(_member_started.call_count, 6) + self.assertEqual(_cluster_members.call_count, 6) + mock_event.defer.assert_called_once() + _set_unit_completed.assert_not_called() + _set_unit_failed.assert_not_called() + + # Test when the member has already joined the cluster. + _member_started.reset_mock() + _set_unit_failed.reset_mock() + mock_event.defer.reset_mock() + _cluster_members.return_value = [ + self.charm.unit.name.replace("/", "-"), + "postgresql-1", + ] + self.charm.upgrade._on_upgrade_granted(mock_event) + _member_started.assert_called_once() + mock_event.defer.assert_not_called() + _set_unit_completed.assert_called_once() + _set_unit_failed.assert_not_called() + + @patch_network_get(private_address="1.1.1.1") + @patch("charm.Patroni.is_creating_backup", new_callable=PropertyMock) + @patch("charm.Patroni.are_all_members_ready") + def test_pre_upgrade_check( + self, + _are_all_members_ready, + _is_creating_backup, + ): + with self.harness.hooks_disabled(): + self.harness.set_leader(True) + + # Set some side effects to test multiple situations. + _are_all_members_ready.side_effect = [False, True, True] + _is_creating_backup.side_effect = [True, False, False] + + # Test when not all members are ready. + with self.assertRaises(ClusterNotReadyError): + self.charm.upgrade.pre_upgrade_check() + + # Test when a backup is being created. + with self.assertRaises(ClusterNotReadyError): + self.charm.upgrade.pre_upgrade_check() + + # Test when everything is ok to start the upgrade. + self.charm.upgrade.pre_upgrade_check() From 0f2f94227e72bfb14fe43a9bda53f654ec4df719 Mon Sep 17 00:00:00 2001 From: Marcelo Henrique Neppel Date: Tue, 25 Jul 2023 17:00:45 -0300 Subject: [PATCH 04/13] Added one more check in unit test --- tests/unit/test_upgrade.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/unit/test_upgrade.py b/tests/unit/test_upgrade.py index f0b865a86e..a5133af196 100644 --- a/tests/unit/test_upgrade.py +++ b/tests/unit/test_upgrade.py @@ -51,6 +51,7 @@ def test_log_rollback(self, mock_logging, _update_config): ) @patch_network_get(private_address="1.1.1.1") + @patch("charms.data_platform_libs.v0.upgrade.DataUpgrade.on_upgrade_changed") @patch("charms.data_platform_libs.v0.upgrade.DataUpgrade.set_unit_failed") @patch("charms.data_platform_libs.v0.upgrade.DataUpgrade.set_unit_completed") @patch("charm.Patroni.cluster_members", new_callable=PropertyMock) @@ -71,6 +72,7 @@ def test_on_upgrade_granted( _cluster_members, _set_unit_completed, _set_unit_failed, + _on_upgrade_changed, ): # Test when the charm fails to start Patroni. mock_event = MagicMock() @@ -81,6 +83,7 @@ def test_on_upgrade_granted( mock_event.defer.assert_not_called() _set_unit_completed.assert_not_called() _set_unit_failed.assert_called_once() + _on_upgrade_changed.assert_not_called() # Test when the member hasn't started yet. _set_unit_failed.reset_mock() @@ -92,6 +95,7 @@ def test_on_upgrade_granted( mock_event.defer.assert_called_once() _set_unit_completed.assert_not_called() _set_unit_failed.assert_not_called() + _on_upgrade_changed.assert_not_called() # Test when the member has already started but not joined the cluster yet. _member_started.reset_mock() @@ -104,9 +108,11 @@ def test_on_upgrade_granted( mock_event.defer.assert_called_once() _set_unit_completed.assert_not_called() _set_unit_failed.assert_not_called() + _on_upgrade_changed.assert_not_called() # Test when the member has already joined the cluster. _member_started.reset_mock() + _cluster_members.reset_mock() _set_unit_failed.reset_mock() mock_event.defer.reset_mock() _cluster_members.return_value = [ @@ -115,9 +121,26 @@ def test_on_upgrade_granted( ] self.charm.upgrade._on_upgrade_granted(mock_event) _member_started.assert_called_once() + _cluster_members.assert_called_once() mock_event.defer.assert_not_called() _set_unit_completed.assert_called_once() _set_unit_failed.assert_not_called() + _on_upgrade_changed.assert_not_called() + + # Test when the member is the leader. + _member_started.reset_mock() + _cluster_members.reset_mock() + _set_unit_completed.reset_mock() + _set_unit_failed.reset_mock() + with self.harness.hooks_disabled(): + self.harness.set_leader(True) + self.charm.upgrade._on_upgrade_granted(mock_event) + _member_started.assert_called_once() + _cluster_members.assert_called_once() + mock_event.defer.assert_not_called() + _set_unit_completed.assert_called_once() + _set_unit_failed.assert_not_called() + _on_upgrade_changed.assert_called_once() @patch_network_get(private_address="1.1.1.1") @patch("charm.Patroni.is_creating_backup", new_callable=PropertyMock) From 339830b92ca2d2d341929f6f4a285a00b8e70ced Mon Sep 17 00:00:00 2001 From: Marcelo Henrique Neppel Date: Tue, 25 Jul 2023 17:09:54 -0300 Subject: [PATCH 05/13] Removed upgrade integration tests --- .github/workflows/ci.yaml | 1 - tests/integration/conftest.py | 18 ------- tests/integration/test_upgrade.py | 82 ------------------------------- tox.ini | 10 ---- 4 files changed, 111 deletions(-) delete mode 100644 tests/integration/test_upgrade.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 926d605808..0cdf20a51d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -83,7 +83,6 @@ jobs: - password-rotation-integration - plugins-integration - tls-integration - - upgrade-integration name: ${{ matrix.tox-environments }} needs: - lib-check diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 89b4fe2d57..4e90a1321b 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -9,9 +9,7 @@ import boto3 import pytest from pytest_operator.plugin import OpsTest -from tenacity import Retrying, stop_after_delay, wait_fixed -from tests.integration.ha_tests.conftest import APPLICATION_NAME from tests.integration.helpers import construct_endpoint AWS = "AWS" @@ -62,22 +60,6 @@ async def cloud_configs(ops_test: OpsTest) -> None: bucket_object.delete() -@pytest.fixture() -async def continuous_writes(ops_test: OpsTest) -> None: - """Deploy the charm that makes continuous writes to PostgreSQL.""" - yield - # Clear the written data at the end. - for attempt in Retrying(stop=stop_after_delay(60 * 5), wait=wait_fixed(3), reraise=True): - with attempt: - action = ( - await ops_test.model.applications[APPLICATION_NAME] - .units[0] - .run_action("clear-continuous-writes") - ) - await action.wait() - assert action.results["result"] == "True", "Unable to clear up continuous_writes table" - - @pytest.fixture(scope="module") def ops_test(ops_test: OpsTest) -> OpsTest: if os.environ.get("CI") == "true": diff --git a/tests/integration/test_upgrade.py b/tests/integration/test_upgrade.py deleted file mode 100644 index 5a1ea49f2f..0000000000 --- a/tests/integration/test_upgrade.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. -import logging - -import pytest as pytest -from pytest_operator.plugin import OpsTest - -from tests.integration.ha_tests.conftest import APPLICATION_NAME -from tests.integration.ha_tests.helpers import ( - app_name, - are_writes_increasing, - check_writes, - start_continuous_writes, -) -from tests.integration.helpers import get_primary - -logger = logging.getLogger(__name__) - - -@pytest.mark.abort_on_fail -async def test_build_and_deploy(ops_test: OpsTest) -> None: - """Build and deploy three unit of PostgreSQL.""" - wait_for_apps = False - # Check if there is a pre-existing cluster. - if not await app_name(ops_test): - wait_for_apps = True - charm = await ops_test.build_charm(".") - async with ops_test.fast_forward(): - await ops_test.model.deploy(charm, num_units=3) - # Deploy the continuous writes application charm if it wasn't already deployed. - if not await app_name(ops_test, APPLICATION_NAME): - wait_for_apps = True - async with ops_test.fast_forward(): - charm = await ops_test.build_charm("tests/integration/ha_tests/application-charm") - await ops_test.model.deploy(charm, application_name=APPLICATION_NAME) - - if wait_for_apps: - async with ops_test.fast_forward(): - await ops_test.model.wait_for_idle(status="active", timeout=1000) - - -async def test_upgrade(ops_test: OpsTest, continuous_writes) -> None: - # Start an application that continuously writes data to the database. - logger.info("starting continuous writes to the database") - app = await app_name(ops_test) - await start_continuous_writes(ops_test, app) - - # Check whether writes are increasing. - logger.info("checking whether writes are increasing") - any_unit_name = next(iter(ops_test.model.applications[app].units)).name - primary_name = await get_primary(ops_test, any_unit_name) - await are_writes_increasing(ops_test, primary_name) - - # Run the pre-upgrade-check action. - logger.info("running pre-upgrade check") - leader_unit_name = None - for unit in ops_test.model.applications[app].units: - if await unit.is_leader_from_status(): - leader_unit_name = unit.name - break - action = await ops_test.model.units.get(leader_unit_name).run_action("pre-upgrade-check") - await action.wait() - assert action.results["Code"] == "0" - - # Run juju refresh. - logger.info("refreshing the charm") - application = ops_test.model.applications[app] - charm = await ops_test.build_charm(".") - await application.refresh(path=charm) - async with ops_test.fast_forward(fast_interval="30s"): - await ops_test.model.wait_for_idle(apps=[app], status="active", idle_period=15) - - # Check whether writes are increasing. - logger.info("checking whether writes are increasing") - primary_name = await get_primary(ops_test, any_unit_name) - await are_writes_increasing(ops_test, primary_name) - - # Verify that no writes to the database were missed after stopping the writes - # (check that all the units have all the writes). - logger.info("checking whether no writes were lost") - await check_writes(ops_test) diff --git a/tox.ini b/tox.ini index a9604b8b0e..63a92519b5 100644 --- a/tox.ini +++ b/tox.ini @@ -172,16 +172,6 @@ commands = poetry install --with integration poetry run pytest -v --tb native --log-cli-level=INFO -s {posargs} {[vars]tests_path}/integration/test_tls.py -[testenv:upgrade-integration] -description = Run upgrade integration tests -pass_env = - {[testenv]pass_env} - CI - CI_PACKED_CHARMS -commands = - poetry install --with integration - poetry run pytest -v --tb native --log-cli-level=INFO -s {posargs} {[vars]tests_path}/integration/test_upgrade.py - [testenv:integration] description = Run all integration tests pass_env = From fc35a4b2ac8c2aae34589e7200e97dc8c074ff51 Mon Sep 17 00:00:00 2001 From: Marcelo Henrique Neppel Date: Tue, 25 Jul 2023 17:20:09 -0300 Subject: [PATCH 06/13] Revert "Removed upgrade integration tests" This reverts commit 339830b92ca2d2d341929f6f4a285a00b8e70ced. --- .github/workflows/ci.yaml | 1 + tests/integration/conftest.py | 18 +++++++ tests/integration/test_upgrade.py | 82 +++++++++++++++++++++++++++++++ tox.ini | 10 ++++ 4 files changed, 111 insertions(+) create mode 100644 tests/integration/test_upgrade.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0cdf20a51d..926d605808 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -83,6 +83,7 @@ jobs: - password-rotation-integration - plugins-integration - tls-integration + - upgrade-integration name: ${{ matrix.tox-environments }} needs: - lib-check diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 4e90a1321b..89b4fe2d57 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -9,7 +9,9 @@ import boto3 import pytest from pytest_operator.plugin import OpsTest +from tenacity import Retrying, stop_after_delay, wait_fixed +from tests.integration.ha_tests.conftest import APPLICATION_NAME from tests.integration.helpers import construct_endpoint AWS = "AWS" @@ -60,6 +62,22 @@ async def cloud_configs(ops_test: OpsTest) -> None: bucket_object.delete() +@pytest.fixture() +async def continuous_writes(ops_test: OpsTest) -> None: + """Deploy the charm that makes continuous writes to PostgreSQL.""" + yield + # Clear the written data at the end. + for attempt in Retrying(stop=stop_after_delay(60 * 5), wait=wait_fixed(3), reraise=True): + with attempt: + action = ( + await ops_test.model.applications[APPLICATION_NAME] + .units[0] + .run_action("clear-continuous-writes") + ) + await action.wait() + assert action.results["result"] == "True", "Unable to clear up continuous_writes table" + + @pytest.fixture(scope="module") def ops_test(ops_test: OpsTest) -> OpsTest: if os.environ.get("CI") == "true": diff --git a/tests/integration/test_upgrade.py b/tests/integration/test_upgrade.py new file mode 100644 index 0000000000..5a1ea49f2f --- /dev/null +++ b/tests/integration/test_upgrade.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. +import logging + +import pytest as pytest +from pytest_operator.plugin import OpsTest + +from tests.integration.ha_tests.conftest import APPLICATION_NAME +from tests.integration.ha_tests.helpers import ( + app_name, + are_writes_increasing, + check_writes, + start_continuous_writes, +) +from tests.integration.helpers import get_primary + +logger = logging.getLogger(__name__) + + +@pytest.mark.abort_on_fail +async def test_build_and_deploy(ops_test: OpsTest) -> None: + """Build and deploy three unit of PostgreSQL.""" + wait_for_apps = False + # Check if there is a pre-existing cluster. + if not await app_name(ops_test): + wait_for_apps = True + charm = await ops_test.build_charm(".") + async with ops_test.fast_forward(): + await ops_test.model.deploy(charm, num_units=3) + # Deploy the continuous writes application charm if it wasn't already deployed. + if not await app_name(ops_test, APPLICATION_NAME): + wait_for_apps = True + async with ops_test.fast_forward(): + charm = await ops_test.build_charm("tests/integration/ha_tests/application-charm") + await ops_test.model.deploy(charm, application_name=APPLICATION_NAME) + + if wait_for_apps: + async with ops_test.fast_forward(): + await ops_test.model.wait_for_idle(status="active", timeout=1000) + + +async def test_upgrade(ops_test: OpsTest, continuous_writes) -> None: + # Start an application that continuously writes data to the database. + logger.info("starting continuous writes to the database") + app = await app_name(ops_test) + await start_continuous_writes(ops_test, app) + + # Check whether writes are increasing. + logger.info("checking whether writes are increasing") + any_unit_name = next(iter(ops_test.model.applications[app].units)).name + primary_name = await get_primary(ops_test, any_unit_name) + await are_writes_increasing(ops_test, primary_name) + + # Run the pre-upgrade-check action. + logger.info("running pre-upgrade check") + leader_unit_name = None + for unit in ops_test.model.applications[app].units: + if await unit.is_leader_from_status(): + leader_unit_name = unit.name + break + action = await ops_test.model.units.get(leader_unit_name).run_action("pre-upgrade-check") + await action.wait() + assert action.results["Code"] == "0" + + # Run juju refresh. + logger.info("refreshing the charm") + application = ops_test.model.applications[app] + charm = await ops_test.build_charm(".") + await application.refresh(path=charm) + async with ops_test.fast_forward(fast_interval="30s"): + await ops_test.model.wait_for_idle(apps=[app], status="active", idle_period=15) + + # Check whether writes are increasing. + logger.info("checking whether writes are increasing") + primary_name = await get_primary(ops_test, any_unit_name) + await are_writes_increasing(ops_test, primary_name) + + # Verify that no writes to the database were missed after stopping the writes + # (check that all the units have all the writes). + logger.info("checking whether no writes were lost") + await check_writes(ops_test) diff --git a/tox.ini b/tox.ini index 63a92519b5..a9604b8b0e 100644 --- a/tox.ini +++ b/tox.ini @@ -172,6 +172,16 @@ commands = poetry install --with integration poetry run pytest -v --tb native --log-cli-level=INFO -s {posargs} {[vars]tests_path}/integration/test_tls.py +[testenv:upgrade-integration] +description = Run upgrade integration tests +pass_env = + {[testenv]pass_env} + CI + CI_PACKED_CHARMS +commands = + poetry install --with integration + poetry run pytest -v --tb native --log-cli-level=INFO -s {posargs} {[vars]tests_path}/integration/test_upgrade.py + [testenv:integration] description = Run all integration tests pass_env = From 6ad056268d7c845e5063189f3d77f55c2aa0d48e Mon Sep 17 00:00:00 2001 From: Marcelo Henrique Neppel Date: Fri, 28 Jul 2023 18:58:30 -0300 Subject: [PATCH 07/13] Added replication health check and snap dependency --- lib/charms/data_platform_libs/v0/upgrade.py | 146 ++++++++++++++------ src/cluster.py | 23 +++ src/dependency.json | 6 + src/upgrade.py | 1 + tests/unit/test_cluster.py | 19 ++- tests/unit/test_upgrade.py | 15 +- 6 files changed, 168 insertions(+), 42 deletions(-) diff --git a/lib/charms/data_platform_libs/v0/upgrade.py b/lib/charms/data_platform_libs/v0/upgrade.py index 90fd92f2d3..a83105cf69 100644 --- a/lib/charms/data_platform_libs/v0/upgrade.py +++ b/lib/charms/data_platform_libs/v0/upgrade.py @@ -17,7 +17,7 @@ import json import logging from abc import ABC, abstractmethod -from typing import Iterable, List, Literal, Optional, Tuple +from typing import List, Literal, Optional, Set, Tuple from ops.charm import ( ActionEvent, @@ -27,7 +27,7 @@ UpgradeCharmEvent, ) from ops.framework import EventBase, EventSource, Object -from ops.model import Relation, Unit +from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, Relation, Unit, WaitingStatus from pydantic import BaseModel, root_validator, validator # The unique Charmhub library identifier, never change it @@ -38,7 +38,7 @@ # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 6 +LIBPATCH = 8 PYDEPS = ["pydantic>=1.10,<2"] @@ -435,7 +435,7 @@ class UpgradeEvents(CharmEvents): class DataUpgrade(Object, ABC): """Manages `upgrade` relation operators for in-place upgrades.""" - STATES = ["failed", "idle", "ready", "upgrading", "completed"] + STATES = ["recovery", "failed", "idle", "ready", "upgrading", "completed"] on = UpgradeEvents() # pyright: ignore [reportGeneralTypeIssues] @@ -479,10 +479,10 @@ def peer_relation(self) -> Optional[Relation]: return self.charm.model.get_relation(self.relation_name) @property - def app_units(self) -> Iterable[Unit]: + def app_units(self) -> Set[Unit]: """The peer-related units in the application.""" if not self.peer_relation: - return [] + return set() return set([self.charm.unit] + list(self.peer_relation.units)) @@ -542,6 +542,18 @@ def upgrade_stack(self, stack: List[int]) -> None: self.peer_relation.data[self.charm.app].update({"upgrade-stack": json.dumps(stack)}) self._upgrade_stack = stack + @property + def unit_states(self) -> list: + """Current upgrade state for all units. + + Returns: + Unsorted list of upgrade states for all units. + """ + if not self.peer_relation: + return [] + + return [self.peer_relation.data[unit].get("state", "") for unit in self.app_units] + @property def cluster_state(self) -> Optional[str]: """Current upgrade state for cluster units. @@ -554,13 +566,11 @@ def cluster_state(self) -> Optional[str]: Returns: String of upgrade state from the furthest behind unit. """ - if not self.peer_relation: + if not self.unit_states: return None - states = [self.peer_relation.data[unit].get("state", "") for unit in self.app_units] - try: - return sorted(states, key=self.STATES.index)[0] + return sorted(self.unit_states, key=self.STATES.index)[0] except (ValueError, KeyError): return None @@ -598,8 +608,36 @@ def log_rollback_instructions(self) -> None: """ pass - def set_unit_failed(self) -> None: - """Sets unit `state=failed` to the upgrade peer data.""" + def _repair_upgrade_stack(self) -> None: + """Ensures completed units are re-added to the upgrade-stack after failure.""" + # need to update the stack as it was not refreshed by rollback run of pre-upgrade-check + # avoids difficult health check implementation by charm-authors needing to exclude dead units + + # if the first unit in the stack fails, the stack will be the same length as units + # i.e this block not ran + if ( + self.cluster_state in ["failed", "recovery"] + and self.upgrade_stack + and len(self.upgrade_stack) != len(self.app_units) + and self.charm.unit.is_leader() + ): + new_stack = self.upgrade_stack + for unit in self.app_units: + unit_id = int(unit.name.split("/")[1]) + + # if a unit fails, it rolls back first + if unit_id not in new_stack: + new_stack.insert(-1, unit_id) + logger.debug(f"Inserted {unit_id} in to upgrade-stack - {new_stack}") + + self.upgrade_stack = new_stack + + def set_unit_failed(self, cause: Optional[str] = None) -> None: + """Sets unit `state=failed` to the upgrade peer data. + + Args: + cause: short description of cause of failure + """ if not self.peer_relation: return None @@ -608,7 +646,9 @@ def set_unit_failed(self) -> None: if self.charm.unit.is_leader(): self._upgrade_stack = None + self.charm.unit.status = BlockedStatus(cause if cause else "") self.peer_relation.data[self.charm.unit].update({"state": "failed"}) + self.log_rollback_instructions() def set_unit_completed(self) -> None: """Sets unit `state=completed` to the upgrade peer data.""" @@ -620,6 +660,7 @@ def set_unit_completed(self) -> None: if self.charm.unit.is_leader(): self._upgrade_stack = None + self.charm.unit.status = MaintenanceStatus("upgrade completed") self.peer_relation.data[self.charm.unit].update({"state": "completed"}) # Emit upgrade_finished event to run unit's post upgrade operations. @@ -654,6 +695,13 @@ def _on_pre_upgrade_check_action(self, event: ActionEvent) -> None: event.fail(message="Action must be ran on the Juju leader.") return + if self.cluster_state == "failed": + logger.info("Entering recovery state for rolling-back to previous version...") + self._repair_upgrade_stack() + self.charm.unit.status = BlockedStatus("ready to rollback application") + self.peer_relation.data[self.charm.unit].update({"state": "recovery"}) + return + # checking if upgrade in progress if self.cluster_state != "idle": event.fail("Cannot run pre-upgrade checks, cluster already upgrading.") @@ -758,47 +806,58 @@ def _on_upgrade_charm(self, event: UpgradeCharmEvent) -> None: event.defer() return - # if any other unit failed or if no stack (i.e pre-upgrade check), mark failed - if not self.upgrade_stack or self.cluster_state == "failed": - logger.error( - "Cluster upgrade failed. Setting failed upgrade state... {}".format( - "Ensure pre-upgrade checks are ran first" if not self.upgrade_stack else "" - ) - ) - self.set_unit_failed() - self.log_rollback_instructions() + if not self.upgrade_stack: + logger.error("Cluster upgrade failed, ensure pre-upgrade checks are ran first.") return - # run version checks on leader only - if self.charm.unit.is_leader(): - try: - self._upgrade_supported_check() - except VersionError as e: # not ready if not passed check - logger.error(e) - self.set_unit_failed() - return + if self.substrate == "vm": + # for VM run version checks on leader only + if self.charm.unit.is_leader(): + try: + self._upgrade_supported_check() + except VersionError as e: # not ready if not passed check + logger.error(e) + self.set_unit_failed() + return + self.charm.unit.status = WaitingStatus("other units upgrading first...") + self.peer_relation.data[self.charm.unit].update({"state": "ready"}) - # all units sets state to ready - self.peer_relation.data[self.charm.unit].update( - {"state": "ready" if self.substrate == "vm" else "upgrading"} - ) + else: + # for k8s run version checks only on highest ordinal unit + if ( + self.charm.unit.name + == f"{self.charm.app.name}/{self.charm.app.planned_units() -1}" + ): + try: + self._upgrade_supported_check() + except VersionError as e: # not ready if not passed check + logger.error(e) + self.set_unit_failed() + return + # On K8s an unit that receives the upgrade-charm event is upgrading + self.charm.unit.status = MaintenanceStatus("upgrading unit") + self.peer_relation.data[self.charm.unit].update({"state": "upgrading"}) def on_upgrade_changed(self, event: EventBase) -> None: """Handler for `upgrade-relation-changed` events.""" if not self.peer_relation: return - # if any other unit failed, mark as failed + # if any other unit failed, don't continue with upgrade if self.cluster_state == "failed": - logger.error("Cluster upgrade failed. Setting failed upgrade state...") - self.set_unit_failed() - self.log_rollback_instructions() + logger.debug("Cluster failed to upgrade, exiting...") + return + + if self.cluster_state == "recovery": + logger.debug("Cluster in recovery, deferring...") + event.defer() return # if all units completed, mark as complete if not self.upgrade_stack: if self.state == "completed" and self.cluster_state in ["idle", "completed"]: logger.info("All units completed upgrade, setting idle upgrade state...") + self.charm.unit.status = ActiveStatus() self.peer_relation.data[self.charm.unit].update({"state": "idle"}) return if self.cluster_state == "idle": @@ -808,6 +867,10 @@ def on_upgrade_changed(self, event: EventBase) -> None: logger.debug("Did not find upgrade-stack or completed cluster state, deferring...") event.defer() return + else: + # upgrade ongoing, set status for waiting units + if "upgrading" in self.unit_states and self.state in ["idle", "ready"]: + self.charm.unit.status = WaitingStatus("other units upgrading first...") # pop mutates the `upgrade_stack` attr top_unit_id = self.upgrade_stack.pop() @@ -827,11 +890,16 @@ def on_upgrade_changed(self, event: EventBase) -> None: # in case leader is next or the last unit to complete self.on_upgrade_changed(event) - # if unit top of stack, emit granted event - if self.charm.unit == top_unit and top_state in ["ready", "upgrading"]: + # if unit top of stack and all units ready (i.e stack), emit granted event + if ( + self.charm.unit == top_unit + and top_state in ["ready", "upgrading"] + and self.cluster_state == "ready" + ): logger.debug( f"{top_unit.name} is next to upgrade, emitting `upgrade_granted` event and upgrading..." ) + self.charm.unit.status = MaintenanceStatus("upgrading...") self.peer_relation.data[self.charm.unit].update({"state": "upgrading"}) getattr(self.on, "upgrade_granted").emit() diff --git a/src/cluster.py b/src/cluster.py index e02a5b1561..4326c445bf 100644 --- a/src/cluster.py +++ b/src/cluster.py @@ -342,6 +342,29 @@ def is_creating_backup(self) -> bool: for member in r.json()["members"] ) + @property + def is_replication_healthy(self) -> bool: + """Return whether the replication is healthy.""" + try: + for attempt in Retrying(stop=stop_after_delay(60), wait=wait_fixed(3)): + with attempt: + primary = self.get_primary() + primary_ip = self.get_member_ip(primary) + members_ips = {self.unit_ip} + members_ips.update(self.peers_ips) + for members_ip in members_ips: + endpoint = "leader" if members_ip == primary_ip else "replica?lag=16kB" + url = self._patroni_url.replace(self.unit_ip, members_ip) + member_status = requests.get(f"{url}/{endpoint}", verify=self.verify) + if member_status.status_code != 200: + raise Exception + except RetryError: + logger.exception("replication is not healthy") + return False + + logger.debug("replication is healthy") + return True + @property def member_started(self) -> bool: """Has the member started Patroni and PostgreSQL. diff --git a/src/dependency.json b/src/dependency.json index fde65758d5..a24f6fad05 100644 --- a/src/dependency.json +++ b/src/dependency.json @@ -4,5 +4,11 @@ "name": "postgresql", "upgrade_supported": ">0", "version": "1" + }, + "snap": { + "dependencies": {}, + "name": "charmed-postgresql", + "upgrade_supported": "^14", + "version": "14.8" } } diff --git a/src/upgrade.py b/src/upgrade.py index 0936d59ae7..a01abaaa9a 100644 --- a/src/upgrade.py +++ b/src/upgrade.py @@ -101,6 +101,7 @@ def _on_upgrade_granted(self, event: UpgradeGrantedEvent) -> None: not self.charm._patroni.member_started or self.charm.unit.name.replace("/", "-") not in self.charm._patroni.cluster_members + or not self.charm._patroni.is_replication_healthy ): logger.debug( "Instance not yet back in the cluster." diff --git a/tests/unit/test_cluster.py b/tests/unit/test_cluster.py index 6cf8860d2f..d460cf0d9b 100644 --- a/tests/unit/test_cluster.py +++ b/tests/unit/test_cluster.py @@ -2,12 +2,13 @@ # See LICENSE file for licensing details. import unittest -from unittest.mock import Mock, PropertyMock, mock_open, patch, sentinel +from unittest.mock import MagicMock, Mock, PropertyMock, mock_open, patch, sentinel import requests as requests import tenacity as tenacity from charms.operator_libs_linux.v2 import snap from jinja2 import Template +from tenacity import stop_after_delay from cluster import Patroni from constants import ( @@ -163,6 +164,22 @@ def test_is_creating_backup(self, _get): } self.assertFalse(self.patroni.is_creating_backup) + @patch("requests.get") + @patch("charm.Patroni.get_primary") + @patch("cluster.stop_after_delay", return_value=stop_after_delay(0)) + def test_is_replication_healthy(self, _, __, _get): + # Test when replication is healthy. + _get.return_value.status_code = 200 + self.assertTrue(self.patroni.is_replication_healthy) + + # Test when replication is not healthy. + _get.side_effect = [ + MagicMock(status_code=200), + MagicMock(status_code=200), + MagicMock(status_code=503), + ] + self.assertFalse(self.patroni.is_replication_healthy) + @patch("cluster.stop_after_delay", return_value=tenacity.stop_after_delay(0)) @patch("cluster.wait_fixed", return_value=tenacity.wait_fixed(0)) @patch("requests.get", side_effect=mocked_requests_get) diff --git a/tests/unit/test_upgrade.py b/tests/unit/test_upgrade.py index a5133af196..9402ad1295 100644 --- a/tests/unit/test_upgrade.py +++ b/tests/unit/test_upgrade.py @@ -54,6 +54,7 @@ def test_log_rollback(self, mock_logging, _update_config): @patch("charms.data_platform_libs.v0.upgrade.DataUpgrade.on_upgrade_changed") @patch("charms.data_platform_libs.v0.upgrade.DataUpgrade.set_unit_failed") @patch("charms.data_platform_libs.v0.upgrade.DataUpgrade.set_unit_completed") + @patch("charm.Patroni.is_replication_healthy", new_callable=PropertyMock) @patch("charm.Patroni.cluster_members", new_callable=PropertyMock) @patch("charm.Patroni.member_started", new_callable=PropertyMock) @patch("upgrade.wait_fixed", return_value=tenacity.wait_fixed(0)) @@ -70,6 +71,7 @@ def test_on_upgrade_granted( _, _member_started, _cluster_members, + _is_replication_healthy, _set_unit_completed, _set_unit_failed, _on_upgrade_changed, @@ -127,11 +129,20 @@ def test_on_upgrade_granted( _set_unit_failed.assert_not_called() _on_upgrade_changed.assert_not_called() + # Test when the member has already joined the cluster but the replication + # is not healthy yet. + _set_unit_completed.reset_mock() + _is_replication_healthy.return_value = False + self.charm.upgrade._on_upgrade_granted(mock_event) + mock_event.defer.assert_called_once() + _set_unit_completed.assert_not_called() + _set_unit_failed.assert_not_called() + # Test when the member is the leader. _member_started.reset_mock() _cluster_members.reset_mock() - _set_unit_completed.reset_mock() - _set_unit_failed.reset_mock() + mock_event.defer.reset_mock() + _is_replication_healthy.return_value = True with self.harness.hooks_disabled(): self.harness.set_leader(True) self.charm.upgrade._on_upgrade_granted(mock_event) From 735b65412a97b432cd6eaa77768069023ad8cd4f Mon Sep 17 00:00:00 2001 From: Marcelo Henrique Neppel Date: Wed, 2 Aug 2023 14:35:38 -0300 Subject: [PATCH 08/13] Added replication health check and snap dependency --- lib/charms/data_platform_libs/v0/upgrade.py | 146 ++++++++++++++------ src/cluster.py | 23 +++ src/dependency.json | 6 + src/upgrade.py | 1 + tests/unit/test_cluster.py | 19 ++- tests/unit/test_upgrade.py | 15 +- 6 files changed, 168 insertions(+), 42 deletions(-) diff --git a/lib/charms/data_platform_libs/v0/upgrade.py b/lib/charms/data_platform_libs/v0/upgrade.py index 90fd92f2d3..a83105cf69 100644 --- a/lib/charms/data_platform_libs/v0/upgrade.py +++ b/lib/charms/data_platform_libs/v0/upgrade.py @@ -17,7 +17,7 @@ import json import logging from abc import ABC, abstractmethod -from typing import Iterable, List, Literal, Optional, Tuple +from typing import List, Literal, Optional, Set, Tuple from ops.charm import ( ActionEvent, @@ -27,7 +27,7 @@ UpgradeCharmEvent, ) from ops.framework import EventBase, EventSource, Object -from ops.model import Relation, Unit +from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, Relation, Unit, WaitingStatus from pydantic import BaseModel, root_validator, validator # The unique Charmhub library identifier, never change it @@ -38,7 +38,7 @@ # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 6 +LIBPATCH = 8 PYDEPS = ["pydantic>=1.10,<2"] @@ -435,7 +435,7 @@ class UpgradeEvents(CharmEvents): class DataUpgrade(Object, ABC): """Manages `upgrade` relation operators for in-place upgrades.""" - STATES = ["failed", "idle", "ready", "upgrading", "completed"] + STATES = ["recovery", "failed", "idle", "ready", "upgrading", "completed"] on = UpgradeEvents() # pyright: ignore [reportGeneralTypeIssues] @@ -479,10 +479,10 @@ def peer_relation(self) -> Optional[Relation]: return self.charm.model.get_relation(self.relation_name) @property - def app_units(self) -> Iterable[Unit]: + def app_units(self) -> Set[Unit]: """The peer-related units in the application.""" if not self.peer_relation: - return [] + return set() return set([self.charm.unit] + list(self.peer_relation.units)) @@ -542,6 +542,18 @@ def upgrade_stack(self, stack: List[int]) -> None: self.peer_relation.data[self.charm.app].update({"upgrade-stack": json.dumps(stack)}) self._upgrade_stack = stack + @property + def unit_states(self) -> list: + """Current upgrade state for all units. + + Returns: + Unsorted list of upgrade states for all units. + """ + if not self.peer_relation: + return [] + + return [self.peer_relation.data[unit].get("state", "") for unit in self.app_units] + @property def cluster_state(self) -> Optional[str]: """Current upgrade state for cluster units. @@ -554,13 +566,11 @@ def cluster_state(self) -> Optional[str]: Returns: String of upgrade state from the furthest behind unit. """ - if not self.peer_relation: + if not self.unit_states: return None - states = [self.peer_relation.data[unit].get("state", "") for unit in self.app_units] - try: - return sorted(states, key=self.STATES.index)[0] + return sorted(self.unit_states, key=self.STATES.index)[0] except (ValueError, KeyError): return None @@ -598,8 +608,36 @@ def log_rollback_instructions(self) -> None: """ pass - def set_unit_failed(self) -> None: - """Sets unit `state=failed` to the upgrade peer data.""" + def _repair_upgrade_stack(self) -> None: + """Ensures completed units are re-added to the upgrade-stack after failure.""" + # need to update the stack as it was not refreshed by rollback run of pre-upgrade-check + # avoids difficult health check implementation by charm-authors needing to exclude dead units + + # if the first unit in the stack fails, the stack will be the same length as units + # i.e this block not ran + if ( + self.cluster_state in ["failed", "recovery"] + and self.upgrade_stack + and len(self.upgrade_stack) != len(self.app_units) + and self.charm.unit.is_leader() + ): + new_stack = self.upgrade_stack + for unit in self.app_units: + unit_id = int(unit.name.split("/")[1]) + + # if a unit fails, it rolls back first + if unit_id not in new_stack: + new_stack.insert(-1, unit_id) + logger.debug(f"Inserted {unit_id} in to upgrade-stack - {new_stack}") + + self.upgrade_stack = new_stack + + def set_unit_failed(self, cause: Optional[str] = None) -> None: + """Sets unit `state=failed` to the upgrade peer data. + + Args: + cause: short description of cause of failure + """ if not self.peer_relation: return None @@ -608,7 +646,9 @@ def set_unit_failed(self) -> None: if self.charm.unit.is_leader(): self._upgrade_stack = None + self.charm.unit.status = BlockedStatus(cause if cause else "") self.peer_relation.data[self.charm.unit].update({"state": "failed"}) + self.log_rollback_instructions() def set_unit_completed(self) -> None: """Sets unit `state=completed` to the upgrade peer data.""" @@ -620,6 +660,7 @@ def set_unit_completed(self) -> None: if self.charm.unit.is_leader(): self._upgrade_stack = None + self.charm.unit.status = MaintenanceStatus("upgrade completed") self.peer_relation.data[self.charm.unit].update({"state": "completed"}) # Emit upgrade_finished event to run unit's post upgrade operations. @@ -654,6 +695,13 @@ def _on_pre_upgrade_check_action(self, event: ActionEvent) -> None: event.fail(message="Action must be ran on the Juju leader.") return + if self.cluster_state == "failed": + logger.info("Entering recovery state for rolling-back to previous version...") + self._repair_upgrade_stack() + self.charm.unit.status = BlockedStatus("ready to rollback application") + self.peer_relation.data[self.charm.unit].update({"state": "recovery"}) + return + # checking if upgrade in progress if self.cluster_state != "idle": event.fail("Cannot run pre-upgrade checks, cluster already upgrading.") @@ -758,47 +806,58 @@ def _on_upgrade_charm(self, event: UpgradeCharmEvent) -> None: event.defer() return - # if any other unit failed or if no stack (i.e pre-upgrade check), mark failed - if not self.upgrade_stack or self.cluster_state == "failed": - logger.error( - "Cluster upgrade failed. Setting failed upgrade state... {}".format( - "Ensure pre-upgrade checks are ran first" if not self.upgrade_stack else "" - ) - ) - self.set_unit_failed() - self.log_rollback_instructions() + if not self.upgrade_stack: + logger.error("Cluster upgrade failed, ensure pre-upgrade checks are ran first.") return - # run version checks on leader only - if self.charm.unit.is_leader(): - try: - self._upgrade_supported_check() - except VersionError as e: # not ready if not passed check - logger.error(e) - self.set_unit_failed() - return + if self.substrate == "vm": + # for VM run version checks on leader only + if self.charm.unit.is_leader(): + try: + self._upgrade_supported_check() + except VersionError as e: # not ready if not passed check + logger.error(e) + self.set_unit_failed() + return + self.charm.unit.status = WaitingStatus("other units upgrading first...") + self.peer_relation.data[self.charm.unit].update({"state": "ready"}) - # all units sets state to ready - self.peer_relation.data[self.charm.unit].update( - {"state": "ready" if self.substrate == "vm" else "upgrading"} - ) + else: + # for k8s run version checks only on highest ordinal unit + if ( + self.charm.unit.name + == f"{self.charm.app.name}/{self.charm.app.planned_units() -1}" + ): + try: + self._upgrade_supported_check() + except VersionError as e: # not ready if not passed check + logger.error(e) + self.set_unit_failed() + return + # On K8s an unit that receives the upgrade-charm event is upgrading + self.charm.unit.status = MaintenanceStatus("upgrading unit") + self.peer_relation.data[self.charm.unit].update({"state": "upgrading"}) def on_upgrade_changed(self, event: EventBase) -> None: """Handler for `upgrade-relation-changed` events.""" if not self.peer_relation: return - # if any other unit failed, mark as failed + # if any other unit failed, don't continue with upgrade if self.cluster_state == "failed": - logger.error("Cluster upgrade failed. Setting failed upgrade state...") - self.set_unit_failed() - self.log_rollback_instructions() + logger.debug("Cluster failed to upgrade, exiting...") + return + + if self.cluster_state == "recovery": + logger.debug("Cluster in recovery, deferring...") + event.defer() return # if all units completed, mark as complete if not self.upgrade_stack: if self.state == "completed" and self.cluster_state in ["idle", "completed"]: logger.info("All units completed upgrade, setting idle upgrade state...") + self.charm.unit.status = ActiveStatus() self.peer_relation.data[self.charm.unit].update({"state": "idle"}) return if self.cluster_state == "idle": @@ -808,6 +867,10 @@ def on_upgrade_changed(self, event: EventBase) -> None: logger.debug("Did not find upgrade-stack or completed cluster state, deferring...") event.defer() return + else: + # upgrade ongoing, set status for waiting units + if "upgrading" in self.unit_states and self.state in ["idle", "ready"]: + self.charm.unit.status = WaitingStatus("other units upgrading first...") # pop mutates the `upgrade_stack` attr top_unit_id = self.upgrade_stack.pop() @@ -827,11 +890,16 @@ def on_upgrade_changed(self, event: EventBase) -> None: # in case leader is next or the last unit to complete self.on_upgrade_changed(event) - # if unit top of stack, emit granted event - if self.charm.unit == top_unit and top_state in ["ready", "upgrading"]: + # if unit top of stack and all units ready (i.e stack), emit granted event + if ( + self.charm.unit == top_unit + and top_state in ["ready", "upgrading"] + and self.cluster_state == "ready" + ): logger.debug( f"{top_unit.name} is next to upgrade, emitting `upgrade_granted` event and upgrading..." ) + self.charm.unit.status = MaintenanceStatus("upgrading...") self.peer_relation.data[self.charm.unit].update({"state": "upgrading"}) getattr(self.on, "upgrade_granted").emit() diff --git a/src/cluster.py b/src/cluster.py index e02a5b1561..4326c445bf 100644 --- a/src/cluster.py +++ b/src/cluster.py @@ -342,6 +342,29 @@ def is_creating_backup(self) -> bool: for member in r.json()["members"] ) + @property + def is_replication_healthy(self) -> bool: + """Return whether the replication is healthy.""" + try: + for attempt in Retrying(stop=stop_after_delay(60), wait=wait_fixed(3)): + with attempt: + primary = self.get_primary() + primary_ip = self.get_member_ip(primary) + members_ips = {self.unit_ip} + members_ips.update(self.peers_ips) + for members_ip in members_ips: + endpoint = "leader" if members_ip == primary_ip else "replica?lag=16kB" + url = self._patroni_url.replace(self.unit_ip, members_ip) + member_status = requests.get(f"{url}/{endpoint}", verify=self.verify) + if member_status.status_code != 200: + raise Exception + except RetryError: + logger.exception("replication is not healthy") + return False + + logger.debug("replication is healthy") + return True + @property def member_started(self) -> bool: """Has the member started Patroni and PostgreSQL. diff --git a/src/dependency.json b/src/dependency.json index fde65758d5..a24f6fad05 100644 --- a/src/dependency.json +++ b/src/dependency.json @@ -4,5 +4,11 @@ "name": "postgresql", "upgrade_supported": ">0", "version": "1" + }, + "snap": { + "dependencies": {}, + "name": "charmed-postgresql", + "upgrade_supported": "^14", + "version": "14.8" } } diff --git a/src/upgrade.py b/src/upgrade.py index 0936d59ae7..a01abaaa9a 100644 --- a/src/upgrade.py +++ b/src/upgrade.py @@ -101,6 +101,7 @@ def _on_upgrade_granted(self, event: UpgradeGrantedEvent) -> None: not self.charm._patroni.member_started or self.charm.unit.name.replace("/", "-") not in self.charm._patroni.cluster_members + or not self.charm._patroni.is_replication_healthy ): logger.debug( "Instance not yet back in the cluster." diff --git a/tests/unit/test_cluster.py b/tests/unit/test_cluster.py index 6cf8860d2f..d460cf0d9b 100644 --- a/tests/unit/test_cluster.py +++ b/tests/unit/test_cluster.py @@ -2,12 +2,13 @@ # See LICENSE file for licensing details. import unittest -from unittest.mock import Mock, PropertyMock, mock_open, patch, sentinel +from unittest.mock import MagicMock, Mock, PropertyMock, mock_open, patch, sentinel import requests as requests import tenacity as tenacity from charms.operator_libs_linux.v2 import snap from jinja2 import Template +from tenacity import stop_after_delay from cluster import Patroni from constants import ( @@ -163,6 +164,22 @@ def test_is_creating_backup(self, _get): } self.assertFalse(self.patroni.is_creating_backup) + @patch("requests.get") + @patch("charm.Patroni.get_primary") + @patch("cluster.stop_after_delay", return_value=stop_after_delay(0)) + def test_is_replication_healthy(self, _, __, _get): + # Test when replication is healthy. + _get.return_value.status_code = 200 + self.assertTrue(self.patroni.is_replication_healthy) + + # Test when replication is not healthy. + _get.side_effect = [ + MagicMock(status_code=200), + MagicMock(status_code=200), + MagicMock(status_code=503), + ] + self.assertFalse(self.patroni.is_replication_healthy) + @patch("cluster.stop_after_delay", return_value=tenacity.stop_after_delay(0)) @patch("cluster.wait_fixed", return_value=tenacity.wait_fixed(0)) @patch("requests.get", side_effect=mocked_requests_get) diff --git a/tests/unit/test_upgrade.py b/tests/unit/test_upgrade.py index a5133af196..9402ad1295 100644 --- a/tests/unit/test_upgrade.py +++ b/tests/unit/test_upgrade.py @@ -54,6 +54,7 @@ def test_log_rollback(self, mock_logging, _update_config): @patch("charms.data_platform_libs.v0.upgrade.DataUpgrade.on_upgrade_changed") @patch("charms.data_platform_libs.v0.upgrade.DataUpgrade.set_unit_failed") @patch("charms.data_platform_libs.v0.upgrade.DataUpgrade.set_unit_completed") + @patch("charm.Patroni.is_replication_healthy", new_callable=PropertyMock) @patch("charm.Patroni.cluster_members", new_callable=PropertyMock) @patch("charm.Patroni.member_started", new_callable=PropertyMock) @patch("upgrade.wait_fixed", return_value=tenacity.wait_fixed(0)) @@ -70,6 +71,7 @@ def test_on_upgrade_granted( _, _member_started, _cluster_members, + _is_replication_healthy, _set_unit_completed, _set_unit_failed, _on_upgrade_changed, @@ -127,11 +129,20 @@ def test_on_upgrade_granted( _set_unit_failed.assert_not_called() _on_upgrade_changed.assert_not_called() + # Test when the member has already joined the cluster but the replication + # is not healthy yet. + _set_unit_completed.reset_mock() + _is_replication_healthy.return_value = False + self.charm.upgrade._on_upgrade_granted(mock_event) + mock_event.defer.assert_called_once() + _set_unit_completed.assert_not_called() + _set_unit_failed.assert_not_called() + # Test when the member is the leader. _member_started.reset_mock() _cluster_members.reset_mock() - _set_unit_completed.reset_mock() - _set_unit_failed.reset_mock() + mock_event.defer.reset_mock() + _is_replication_healthy.return_value = True with self.harness.hooks_disabled(): self.harness.set_leader(True) self.charm.upgrade._on_upgrade_granted(mock_event) From 9e97a49331edd6f7816ea58e21aeaa50a5fdd30e Mon Sep 17 00:00:00 2001 From: Marcelo Henrique Neppel Date: Wed, 2 Aug 2023 15:20:43 -0300 Subject: [PATCH 09/13] Remove dependencies version hashes --- requirements.txt | 324 ++---------------- .../application-charm/requirements.txt | 54 +-- .../application-charm/requirements.txt | 50 +-- 3 files changed, 31 insertions(+), 397 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8ec2df1fe1..0fc7adeb6e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,300 +1,24 @@ -boto3==1.28.5 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:2c76db4a1208b8d09814261fc5e530fc36b3b952ef807312495e6869fa6eaad5 \ - --hash=sha256:a5c815ab81219a606f20362c9d9c190f5c224bf33c5dc4c20501036cc4a9034f -botocore==1.31.17 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:396459065dba4339eb4da4ec8b4e6599728eb89b7caaceea199e26f7d824a41c \ - --hash=sha256:6ac34a1d34aa3750e78b77b8596617e2bab938964694d651939dba2cbde2c12b -certifi==2023.7.22 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \ - --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9 -cffi==1.15.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \ - --hash=sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef \ - --hash=sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104 \ - --hash=sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426 \ - --hash=sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405 \ - --hash=sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375 \ - --hash=sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a \ - --hash=sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e \ - --hash=sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc \ - --hash=sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf \ - --hash=sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185 \ - --hash=sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497 \ - --hash=sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3 \ - --hash=sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35 \ - --hash=sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c \ - --hash=sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83 \ - --hash=sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21 \ - --hash=sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca \ - --hash=sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984 \ - --hash=sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac \ - --hash=sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd \ - --hash=sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee \ - --hash=sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a \ - --hash=sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2 \ - --hash=sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192 \ - --hash=sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7 \ - --hash=sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585 \ - --hash=sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f \ - --hash=sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e \ - --hash=sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27 \ - --hash=sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b \ - --hash=sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e \ - --hash=sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e \ - --hash=sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d \ - --hash=sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c \ - --hash=sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415 \ - --hash=sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82 \ - --hash=sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02 \ - --hash=sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314 \ - --hash=sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325 \ - --hash=sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c \ - --hash=sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3 \ - --hash=sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914 \ - --hash=sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045 \ - --hash=sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d \ - --hash=sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9 \ - --hash=sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5 \ - --hash=sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2 \ - --hash=sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c \ - --hash=sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3 \ - --hash=sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2 \ - --hash=sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8 \ - --hash=sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d \ - --hash=sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d \ - --hash=sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9 \ - --hash=sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162 \ - --hash=sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76 \ - --hash=sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4 \ - --hash=sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e \ - --hash=sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9 \ - --hash=sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6 \ - --hash=sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b \ - --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \ - --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0 -charset-normalizer==3.2.0 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96 \ - --hash=sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c \ - --hash=sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710 \ - --hash=sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706 \ - --hash=sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020 \ - --hash=sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252 \ - --hash=sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad \ - --hash=sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329 \ - --hash=sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a \ - --hash=sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f \ - --hash=sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6 \ - --hash=sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4 \ - --hash=sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a \ - --hash=sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46 \ - --hash=sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2 \ - --hash=sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23 \ - --hash=sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace \ - --hash=sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd \ - --hash=sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982 \ - --hash=sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10 \ - --hash=sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2 \ - --hash=sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea \ - --hash=sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09 \ - --hash=sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5 \ - --hash=sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149 \ - --hash=sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489 \ - --hash=sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9 \ - --hash=sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80 \ - --hash=sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592 \ - --hash=sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3 \ - --hash=sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6 \ - --hash=sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed \ - --hash=sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c \ - --hash=sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200 \ - --hash=sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a \ - --hash=sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e \ - --hash=sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d \ - --hash=sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6 \ - --hash=sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623 \ - --hash=sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669 \ - --hash=sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3 \ - --hash=sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa \ - --hash=sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9 \ - --hash=sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2 \ - --hash=sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f \ - --hash=sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1 \ - --hash=sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4 \ - --hash=sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a \ - --hash=sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8 \ - --hash=sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3 \ - --hash=sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029 \ - --hash=sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f \ - --hash=sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959 \ - --hash=sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22 \ - --hash=sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7 \ - --hash=sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952 \ - --hash=sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346 \ - --hash=sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e \ - --hash=sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d \ - --hash=sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299 \ - --hash=sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd \ - --hash=sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a \ - --hash=sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3 \ - --hash=sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037 \ - --hash=sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94 \ - --hash=sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c \ - --hash=sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858 \ - --hash=sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a \ - --hash=sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449 \ - --hash=sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c \ - --hash=sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918 \ - --hash=sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1 \ - --hash=sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c \ - --hash=sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac \ - --hash=sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa -cosl==0.0.5 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:31c131d1f04c061d3fbef49a4e0a175d4cb481deeb06d0cb3c7b242e4c5416be \ - --hash=sha256:84666fde29b792299827d65a1b9b2e3c56029c769e892c8244b50ce793458894 -cryptography==41.0.2 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711 \ - --hash=sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7 \ - --hash=sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd \ - --hash=sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e \ - --hash=sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58 \ - --hash=sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0 \ - --hash=sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d \ - --hash=sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83 \ - --hash=sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831 \ - --hash=sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766 \ - --hash=sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b \ - --hash=sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c \ - --hash=sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182 \ - --hash=sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f \ - --hash=sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa \ - --hash=sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4 \ - --hash=sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a \ - --hash=sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2 \ - --hash=sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76 \ - --hash=sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5 \ - --hash=sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee \ - --hash=sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f \ - --hash=sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14 -idna==3.4 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ - --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 -jmespath==1.0.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \ - --hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe -ops==2.4.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:9cd1a25c6ae30dc9f9afcbe87250f5349ff9a05d5b06632ad8111700c5f0db04 \ - --hash=sha256:b5762bea03049ec150c6266833925d0bb36237b26b006ca878e74e24d162f981 -packaging==23.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \ - --hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f -pgconnstr==1.0.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:0656129961ae879675d0842f5237db82d31ce59c7b3211b051c33e37a864826e \ - --hash=sha256:0f65830e7e3b76adf4390a8592ee52343171a17caef7436257e7bc81c44e21a7 -pycparser==2.21 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ - --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 -pydantic==1.10.11 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:008c5e266c8aada206d0627a011504e14268a62091450210eda7c07fabe6963e \ - --hash=sha256:0588788a9a85f3e5e9ebca14211a496409cb3deca5b6971ff37c556d581854e7 \ - --hash=sha256:08a6c32e1c3809fbc49debb96bf833164f3438b3696abf0fbeceb417d123e6eb \ - --hash=sha256:16928fdc9cb273c6af00d9d5045434c39afba5f42325fb990add2c241402d151 \ - --hash=sha256:174899023337b9fc685ac8adaa7b047050616136ccd30e9070627c1aaab53a13 \ - --hash=sha256:192c608ad002a748e4a0bed2ddbcd98f9b56df50a7c24d9a931a8c5dd053bd3d \ - --hash=sha256:1954f8778489a04b245a1e7b8b22a9d3ea8ef49337285693cf6959e4b757535e \ - --hash=sha256:2417de68290434461a266271fc57274a138510dca19982336639484c73a07af6 \ - --hash=sha256:265a60da42f9f27e0b1014eab8acd3e53bd0bad5c5b4884e98a55f8f596b2c19 \ - --hash=sha256:331c031ba1554b974c98679bd0780d89670d6fd6f53f5d70b10bdc9addee1713 \ - --hash=sha256:373c0840f5c2b5b1ccadd9286782852b901055998136287828731868027a724f \ - --hash=sha256:3f34739a89260dfa420aa3cbd069fbcc794b25bbe5c0a214f8fb29e363484b66 \ - --hash=sha256:41e0bb6efe86281623abbeeb0be64eab740c865388ee934cd3e6a358784aca6e \ - --hash=sha256:4400015f15c9b464c9db2d5d951b6a780102cfa5870f2c036d37c23b56f7fc1b \ - --hash=sha256:44e51ba599c3ef227e168424e220cd3e544288c57829520dc90ea9cb190c3248 \ - --hash=sha256:469adf96c8e2c2bbfa655fc7735a2a82f4c543d9fee97bd113a7fb509bf5e622 \ - --hash=sha256:5b02d24f7b2b365fed586ed73582c20f353a4c50e4be9ba2c57ab96f8091ddae \ - --hash=sha256:7522a7666157aa22b812ce14c827574ddccc94f361237ca6ea8bb0d5c38f1629 \ - --hash=sha256:787cf23e5a0cde753f2eabac1b2e73ae3844eb873fd1f5bdbff3048d8dbb7604 \ - --hash=sha256:8268a735a14c308923e8958363e3a3404f6834bb98c11f5ab43251a4e410170c \ - --hash=sha256:8dc77064471780262b6a68fe67e013298d130414d5aaf9b562c33987dbd2cf4f \ - --hash=sha256:a451ccab49971af043ec4e0d207cbc8cbe53dbf148ef9f19599024076fe9c25b \ - --hash=sha256:a6c098d4ab5e2d5b3984d3cb2527e2d6099d3de85630c8934efcfdc348a9760e \ - --hash=sha256:abade85268cc92dff86d6effcd917893130f0ff516f3d637f50dadc22ae93999 \ - --hash=sha256:bc64eab9b19cd794a380179ac0e6752335e9555d214cfcb755820333c0784cb3 \ - --hash=sha256:c3339a46bbe6013ef7bdd2844679bfe500347ac5742cd4019a88312aa58a9847 \ - --hash=sha256:d185819a7a059550ecb85d5134e7d40f2565f3dd94cfd870132c5f91a89cf58c \ - --hash=sha256:d7781f1d13b19700b7949c5a639c764a077cbbdd4322ed505b449d3ca8edcb36 \ - --hash=sha256:e297897eb4bebde985f72a46a7552a7556a3dd11e7f76acda0c1093e3dbcf216 \ - --hash=sha256:e6cbfbd010b14c8a905a7b10f9fe090068d1744d46f9e0c021db28daeb8b6de1 \ - --hash=sha256:e9738b0f2e6c70f44ee0de53f2089d6002b10c33264abee07bdb5c7f03038303 \ - --hash=sha256:e9baf78b31da2dc3d3f346ef18e58ec5f12f5aaa17ac517e2ffd026a92a87588 \ - --hash=sha256:ef55392ec4bb5721f4ded1096241e4b7151ba6d50a50a80a2526c854f42e6a2f \ - --hash=sha256:f66d479cf7eb331372c470614be6511eae96f1f120344c25f3f9bb59fb1b5528 \ - --hash=sha256:fe429898f2c9dd209bd0632a606bddc06f8bce081bbd03d1c775a45886e2c1cb \ - --hash=sha256:ff44c5e89315b15ff1f7fdaf9853770b810936d6b01a7bcecaa227d2f8fe444f -pyopenssl==23.2.0 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:24f0dc5227396b3e831f4c7f602b950a5e9833d292c8e4a2e06b709292806ae2 \ - --hash=sha256:276f931f55a452e7dea69c7173e984eb2a4407ce413c918aa34b55f82f9b8bac -python-dateutil==2.8.2 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ - --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 -pyyaml==6.0.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ - --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ - --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ - --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ - --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ - --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ - --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ - --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ - --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ - --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ - --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ - --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ - --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ - --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ - --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ - --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ - --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ - --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ - --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ - --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ - --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ - --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ - --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ - --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ - --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ - --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ - --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ - --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ - --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ - --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ - --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ - --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ - --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ - --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ - --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ - --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ - --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ - --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ - --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ - --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f -requests==2.31.0 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ - --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 -s3transfer==0.6.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346 \ - --hash=sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9 -six==1.16.0 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ - --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 -tenacity==8.2.2 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:2f277afb21b851637e8f52e6a613ff08734c347dc19ade928e519d7d2d8569b0 \ - --hash=sha256:43af037822bd0029025877f3b2d97cc4d7bb0c2991000a3d59d71517c5c969e0 -typing-extensions==4.7.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \ - --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2 -urllib3==1.26.16 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f \ - --hash=sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14 -websocket-client==1.6.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:c951af98631d24f8df89ab1019fc365f2227c0892f12fd150e935607c79dd0dd \ - --hash=sha256:f1f9f2ad5291f0225a49efad77abf9e700b6fef553900623060dad6e26503b9d +boto3==1.28.5 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +botocore==1.31.17 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +certifi==2023.7.22 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +cffi==1.15.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +charset-normalizer==3.2.0 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +cosl==0.0.5 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +cryptography==41.0.2 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +idna==3.4 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +jmespath==1.0.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +ops==2.4.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +packaging==23.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +pgconnstr==1.0.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +pycparser==2.21 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +pydantic==1.10.11 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +pyopenssl==23.2.0 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +python-dateutil==2.8.2 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +pyyaml==6.0.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +requests==2.31.0 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +s3transfer==0.6.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +six==1.16.0 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +tenacity==8.2.2 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +typing-extensions==4.7.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +urllib3==1.26.16 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +websocket-client==1.6.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" diff --git a/tests/integration/ha_tests/application-charm/requirements.txt b/tests/integration/ha_tests/application-charm/requirements.txt index e308f7952f..fb73d43ed0 100644 --- a/tests/integration/ha_tests/application-charm/requirements.txt +++ b/tests/integration/ha_tests/application-charm/requirements.txt @@ -1,50 +1,4 @@ -ops==2.4.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:9cd1a25c6ae30dc9f9afcbe87250f5349ff9a05d5b06632ad8111700c5f0db04 \ - --hash=sha256:b5762bea03049ec150c6266833925d0bb36237b26b006ca878e74e24d162f981 -pyyaml==6.0.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ - --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ - --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ - --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ - --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ - --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ - --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ - --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ - --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ - --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ - --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ - --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ - --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ - --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ - --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ - --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ - --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ - --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ - --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ - --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ - --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ - --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ - --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ - --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ - --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ - --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ - --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ - --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ - --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ - --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ - --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ - --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ - --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ - --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ - --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ - --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ - --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ - --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ - --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ - --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f -tenacity==8.2.2 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:2f277afb21b851637e8f52e6a613ff08734c347dc19ade928e519d7d2d8569b0 \ - --hash=sha256:43af037822bd0029025877f3b2d97cc4d7bb0c2991000a3d59d71517c5c969e0 -websocket-client==1.6.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:c951af98631d24f8df89ab1019fc365f2227c0892f12fd150e935607c79dd0dd \ - --hash=sha256:f1f9f2ad5291f0225a49efad77abf9e700b6fef553900623060dad6e26503b9d +ops==2.4.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +pyyaml==6.0.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +tenacity==8.2.2 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +websocket-client==1.6.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" diff --git a/tests/integration/new_relations/application-charm/requirements.txt b/tests/integration/new_relations/application-charm/requirements.txt index cdeb54cdce..0a50b8100e 100644 --- a/tests/integration/new_relations/application-charm/requirements.txt +++ b/tests/integration/new_relations/application-charm/requirements.txt @@ -1,47 +1,3 @@ -ops==2.4.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:9cd1a25c6ae30dc9f9afcbe87250f5349ff9a05d5b06632ad8111700c5f0db04 \ - --hash=sha256:b5762bea03049ec150c6266833925d0bb36237b26b006ca878e74e24d162f981 -pyyaml==6.0.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ - --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ - --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ - --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ - --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ - --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ - --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ - --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ - --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ - --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ - --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ - --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ - --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ - --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ - --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ - --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ - --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ - --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ - --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ - --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ - --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ - --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ - --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ - --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ - --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ - --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ - --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ - --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ - --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ - --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ - --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ - --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ - --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ - --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ - --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ - --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ - --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ - --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ - --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ - --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f -websocket-client==1.6.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" \ - --hash=sha256:c951af98631d24f8df89ab1019fc365f2227c0892f12fd150e935607c79dd0dd \ - --hash=sha256:f1f9f2ad5291f0225a49efad77abf9e700b6fef553900623060dad6e26503b9d +ops==2.4.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +pyyaml==6.0.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" +websocket-client==1.6.1 ; python_full_version >= "3.10.6" and python_full_version < "4.0.0" From 6103cea1cac7f4fc38c10b754e511e9ff62e098c Mon Sep 17 00:00:00 2001 From: Marcelo Henrique Neppel Date: Wed, 16 Aug 2023 16:36:37 -0300 Subject: [PATCH 10/13] Add logic to update dependencies file --- lib/charms/data_platform_libs/v0/upgrade.py | 339 +++++++++++++++++--- src/upgrade.py | 1 + tests/integration/ha_tests/test_upgrade.py | 44 ++- 3 files changed, 345 insertions(+), 39 deletions(-) diff --git a/lib/charms/data_platform_libs/v0/upgrade.py b/lib/charms/data_platform_libs/v0/upgrade.py index a83105cf69..37ae4cc6c0 100644 --- a/lib/charms/data_platform_libs/v0/upgrade.py +++ b/lib/charms/data_platform_libs/v0/upgrade.py @@ -12,7 +12,253 @@ # See the License for the specific language governing permissions and # limitations under the License. -r"""Handler for `upgrade` relation events for in-place upgrades on VMs.""" +r"""Library to manage in-place upgrades for charms running on VMs and K8s. + +This library contains handlers for `upgrade` relation events used to coordinate +between units in an application during a `juju refresh`, as well as `Pydantic` models +for instantiating, validating and comparing dependencies. + +An upgrade on VMs is initiated with the command `juju refresh`. Once executed, the following +events are emitted to each unit at random: + - `upgrade-charm` + - `config-changed` + - `leader-settings-changed` - Non-leader only + +Charm authors can implement the classes defined in this library to streamline the process of +coordinating which unit updates when, achieved through updating of unit-data `state` throughout. + +At a high-level, the upgrade steps are as follows: + - Run pre-checks on the cluster to confirm it is safe to upgrade + - Create stack of unit.ids, to serve as the upgrade order (generally workload leader is last) + - Start the upgrade by issuing a Juju CLI command + - The unit at the top of the stack gets permission to upgrade + - The unit handles the upgrade and restarts their service + - Repeat, until all units have restarted + +### Usage by charm authors + +#### `upgrade` relation + +Charm authors must implement an additional peer-relation. + +As this library uses relation data exchanged between units to coordinate, charm authors +need to add a new relation interface. The relation name does not matter. + +`metadata.yaml` +```yaml +peers: + upgrade: + interface: upgrade +``` + +#### Dependencies JSON/Dict + +Charm authors must implement a dict object tracking current charm versions, requirements + upgradability. + +Many workload versions may be incompatible with older/newer versions. This same idea also can apply to +charm or snap versions. Workloads with required related applications (e.g Kafka + ZooKeeper) also need to +ensure their versions are compatible during an upgrade, to avoid cluster failure. + +As such, it is necessasry to freeze any dependencies within each published charm. An example of this could +be creating a `DEPENDENCIES` dict within the charm code, with the following structure: + +`src/literals.py` +```python +DEPENDENCIES = { + "kafka_charm": { + "dependencies": {"zookeeper": ">50"}, + "name": "kafka", + "upgrade_supported": ">90", + "version": "100", + }, + "kafka_service": { + "dependencies": {"zookeeper": "^3"}, + "name": "kafka", + "upgrade_supported": ">=0.8", + "version": "3.3.2", + }, +} +``` + +The first-level key names are arbitrary labels for tracking what those versions+dependencies are for. +The `dependencies` second-level values are a key-value map of any required external applications, + and the versions this packaged charm can support. +The `upgrade_suppported` second-level values are requirements from which an in-place upgrade can be + supported by the charm. +The `version` second-level values correspond to the current version of this packaged charm. + +Any requirements comply with [`poetry`'s dependency specifications](https://python-poetry.org/docs/dependency-specification/#caret-requirements). + +### Dependency Model + +Charm authors must implement their own class inheriting from `DependencyModel`. + +Using a `Pydantic` model to instantiate the aforementioned `DEPENDENCIES` dict gives stronger type safety and additional +layers of validation. + +Implementation just needs to ensure that the top-level key names from `DEPENDENCIES` are defined as attributed in the model. + +`src/upgrade.py` +```python +from pydantic import BaseModel + +class KafkaDependenciesModel(BaseModel): + kafka_charm: DependencyModel + kafka_service: DependencyModel +``` + +### Overrides for `DataUpgrade` + +Charm authors must define their own class, inheriting from `DataUpgrade`, overriding all required `abstractmethod`s. + +```python +class ZooKeeperUpgrade(DataUpgrade): + def __init__(self, charm: "ZooKeeperUpgrade", **kwargs): + super().__init__(charm, **kwargs) + self.charm = charm +``` + +#### Implementation of `pre_upgrade_check()` + +Before upgrading a cluster, it's a good idea to check that it is stable and healthy before permitting it. +Here, charm authors can validate upgrade safety through API calls, relation-data checks, etc. +If any of these checks fail, raise `ClusterNotReadyError`. + +```python + @override + def pre_upgrade_check(self) -> None: + default_message = "Pre-upgrade check failed and cannot safely upgrade" + try: + if not self.client.members_broadcasting or not len(self.client.server_members) == len( + self.charm.cluster.peer_units + ): + raise ClusterNotReadyError( + message=default_message, + cause="Not all application units are connected and broadcasting in the quorum", + ) + + if self.client.members_syncing: + raise ClusterNotReadyError( + message=default_message, cause="Some quorum members are syncing data" + ) + + if not self.charm.cluster.stable: + raise ClusterNotReadyError( + message=default_message, cause="Charm has not finished initialising" + ) + + except QuorumLeaderNotFoundError: + raise ClusterNotReadyError(message=default_message, cause="Quorum leader not found") + except ConnectionClosedError: + raise ClusterNotReadyError( + message=default_message, cause="Unable to connect to the cluster" + ) +``` + +#### Implementation of `build_upgrade_stack()` - VM ONLY + +Oftentimes, it is necessary to ensure that the workload leader is the last unit to upgrade, +to ensure high-availability during the upgrade process. +Here, charm authors can create a LIFO stack of unit.ids, represented as a list of unit.id strings, +with the leader unit being at i[0]. + +```python +@override +def build_upgrade_stack(self) -> list[int]: + upgrade_stack = [] + for unit in self.charm.cluster.peer_units: + config = self.charm.cluster.unit_config(unit=unit) + + # upgrade quorum leader last + if config["host"] == self.client.leader: + upgrade_stack.insert(0, int(config["unit_id"])) + else: + upgrade_stack.append(int(config["unit_id"])) + + return upgrade_stack +``` + +#### Implementation of `_on_upgrade_granted()` + +On relation-changed events, each unit will check the current upgrade-stack persisted to relation data. +If that unit is at the top of the stack, it will emit an `upgrade-granted` event, which must be handled. +Here, workloads can be re-installed with new versions, checks can be made, data synced etc. +If the new unit successfully rejoined the cluster, call `set_unit_completed()`. +If the new unit failed to rejoin the cluster, call `set_unit_failed()`. + +NOTE - It is essential here to manually call `on_upgrade_changed` if the unit is the current leader. +This ensures that the leader gets it's own relation-changed event, and updates the upgrade-stack for +other units to follow suit. + +```python +@override +def _on_upgrade_granted(self, event: UpgradeGrantedEvent) -> None: + self.charm.snap.stop_snap_service() + + if not self.charm.snap.install(): + logger.error("Unable to install ZooKeeper Snap") + self.set_unit_failed() + return None + + logger.info(f"{self.charm.unit.name} upgrading service...") + self.charm.snap.restart_snap_service() + + try: + logger.debug("Running post-upgrade check...") + self.pre_upgrade_check() + + logger.debug("Marking unit completed...") + self.set_unit_completed() + + # ensures leader gets it's own relation-changed when it upgrades + if self.charm.unit.is_leader(): + logger.debug("Re-emitting upgrade-changed on leader...") + self.on_upgrade_changed(event) + + except ClusterNotReadyError as e: + logger.error(e.cause) + self.set_unit_failed() +``` + +#### Implementation of `log_rollback_instructions()` + +If the upgrade fails, manual intervention may be required for cluster recovery. +Here, charm authors can log out any necessary steps to take to recover from a failed upgrade. +When a unit fails, this library will automatically log out this message. + +```python +@override +def log_rollback_instructions(self) -> None: + logger.error("Upgrade failed. Please run `juju refresh` to previous version.") +``` + +### Instantiating in the charm and deferring events + +Charm authors must add a class attribute for the child class of `DataUpgrade` in the main charm. +They must also ensure that any non-upgrade related events that may be unsafe to handle during +an upgrade, are deferred if the unit is not in the `idle` state - i.e not currently upgrading. + +```python +class ZooKeeperCharm(CharmBase): + def __init__(self, *args): + super().__init__(*args) + self.upgrade = ZooKeeperUpgrade( + self, + relation_name = "upgrade", + substrate = "vm", + dependency_model=ZooKeeperDependencyModel( + **DEPENDENCIES + ), + ) + + def restart(self, event) -> None: + if not self.upgrade.state == "idle": + event.defer() + return None + + self.restart_snap_service() +``` +""" import json import logging @@ -38,7 +284,7 @@ # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 8 +LIBPATCH = 9 PYDEPS = ["pydantic>=1.10,<2"] @@ -77,27 +323,26 @@ def verify_caret_requirements(version: str, requirement: str) -> bool: sem_version = build_complete_sem_ver(version) sem_requirement = build_complete_sem_ver(requirement) - # caret uses first non-zero character, not enough to just count '. - max_version_index = requirement.count(".") - for i, semver in enumerate(sem_requirement): - if semver != 0: - max_version_index = i - break + # caret uses first non-zero character, not enough to just count '.' + if sem_requirement[0] == 0: + max_version_index = requirement.count(".") + for i, semver in enumerate(sem_requirement): + if semver != 0: + max_version_index = i + break + else: + max_version_index = 0 for i in range(3): # version higher than first non-zero - if (i < max_version_index) and (sem_version[i] > sem_requirement[i]): + if (i <= max_version_index) and (sem_version[i] != sem_requirement[i]): return False # version either higher or lower than first non-zero - if (i == max_version_index) and (sem_version[i] != sem_requirement[i]): + if (i > max_version_index) and (sem_version[i] < sem_requirement[i]): return False - # valid - if (i > max_version_index) and (sem_version[i] > sem_requirement[i]): - return True - - return False + return True def verify_tilde_requirements(version: str, requirement: str) -> bool: @@ -403,16 +648,7 @@ def __init__(self, message: str, cause: str, resolution: Optional[str] = None): class UpgradeGrantedEvent(EventBase): - """Used to tell units that they can process an upgrade. - - Handlers of this event must meet the following: - - SHOULD check for related application deps from :class:`DataUpgrade.dependencies` - - MAY raise :class:`DependencyError` if dependency not met - - MUST update unit `state` after validating the success of the upgrade, calling one of: - - :class:`DataUpgrade.set_unit_failed` if the unit upgrade fails - - :class:`DataUpgrade.set_unit_completed` if the unit upgrade succeeds - - MUST call :class:`DataUpgarde.on_upgrade_changed` on exit so event not lost on leader - """ + """Used to tell units that they can process an upgrade.""" class UpgradeFinishedEvent(EventBase): @@ -433,7 +669,7 @@ class UpgradeEvents(CharmEvents): class DataUpgrade(Object, ABC): - """Manages `upgrade` relation operators for in-place upgrades.""" + """Manages `upgrade` relation operations for in-place upgrades.""" STATES = ["recovery", "failed", "idle", "ready", "upgrading", "completed"] @@ -574,6 +810,15 @@ def cluster_state(self) -> Optional[str]: except (ValueError, KeyError): return None + @property + def idle(self) -> Optional[bool]: + """Flag for whether the cluster is in an idle upgrade state. + + Returns: + True if all application units in idle state. Otherwise False + """ + return self.cluster_state == "idle" + @abstractmethod def pre_upgrade_check(self) -> None: """Runs necessary checks validating the cluster is in a healthy state to upgrade. @@ -859,18 +1104,25 @@ def on_upgrade_changed(self, event: EventBase) -> None: logger.info("All units completed upgrade, setting idle upgrade state...") self.charm.unit.status = ActiveStatus() self.peer_relation.data[self.charm.unit].update({"state": "idle"}) + + if self.charm.unit.is_leader(): + logger.debug("Persisting new dependencies to upgrade relation data...") + self.peer_relation.data[self.charm.app].update( + {"dependencies": json.dumps(self.dependency_model.dict())} + ) return + if self.cluster_state == "idle": logger.debug("upgrade-changed event handled before pre-checks, exiting...") return - else: - logger.debug("Did not find upgrade-stack or completed cluster state, deferring...") - event.defer() - return - else: - # upgrade ongoing, set status for waiting units - if "upgrading" in self.unit_states and self.state in ["idle", "ready"]: - self.charm.unit.status = WaitingStatus("other units upgrading first...") + + logger.debug("Did not find upgrade-stack or completed cluster state, deferring...") + event.defer() + return + + # upgrade ongoing, set status for waiting units + if "upgrading" in self.unit_states and self.state in ["idle", "ready"]: + self.charm.unit.status = WaitingStatus("other units upgrading first...") # pop mutates the `upgrade_stack` attr top_unit_id = self.upgrade_stack.pop() @@ -901,10 +1153,25 @@ def on_upgrade_changed(self, event: EventBase) -> None: ) self.charm.unit.status = MaintenanceStatus("upgrading...") self.peer_relation.data[self.charm.unit].update({"state": "upgrading"}) - getattr(self.on, "upgrade_granted").emit() + + try: + getattr(self.on, "upgrade_granted").emit() + except DependencyError as e: + logger.error(e) + self.set_unit_failed() + return def _on_upgrade_granted(self, event: UpgradeGrantedEvent) -> None: - """Handler for `upgrade-granted` events.""" + """Handler for `upgrade-granted` events. + + Handlers of this event must meet the following: + - SHOULD check for related application deps from :class:`DataUpgrade.dependencies` + - MAY raise :class:`DependencyError` if dependency not met + - MUST update unit `state` after validating the success of the upgrade, calling one of: + - :class:`DataUpgrade.set_unit_failed` if the unit upgrade fails + - :class:`DataUpgrade.set_unit_completed` if the unit upgrade succeeds + - MUST call :class:`DataUpgarde.on_upgrade_changed` on exit so event not lost on leader + """ # don't raise if k8s substrate, only return if self.substrate == "k8s": return diff --git a/src/upgrade.py b/src/upgrade.py index a01abaaa9a..1196cd73be 100644 --- a/src/upgrade.py +++ b/src/upgrade.py @@ -26,6 +26,7 @@ class PostgreSQLDependencyModel(BaseModel): """PostgreSQL dependencies model.""" charm: DependencyModel + snap: DependencyModel def get_postgresql_dependencies_model() -> PostgreSQLDependencyModel: diff --git a/tests/integration/ha_tests/test_upgrade.py b/tests/integration/ha_tests/test_upgrade.py index 5a1ea49f2f..86ce914a9c 100644 --- a/tests/integration/ha_tests/test_upgrade.py +++ b/tests/integration/ha_tests/test_upgrade.py @@ -1,7 +1,10 @@ #!/usr/bin/env python3 # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. +import json import logging +import subprocess +import zipfile import pytest as pytest from pytest_operator.plugin import OpsTest @@ -65,11 +68,46 @@ async def test_upgrade(ops_test: OpsTest, continuous_writes) -> None: # Run juju refresh. logger.info("refreshing the charm") - application = ops_test.model.applications[app] charm = await ops_test.build_charm(".") - await application.refresh(path=charm) + print(f"charm: {charm}") + modified_charm = f"{charm}.modified" + print(f"modified_charm: {modified_charm}") + with zipfile.ZipFile(charm, "r") as charm_file, zipfile.ZipFile( + modified_charm, "w" + ) as modified_charm_file: + # Iterate the input files + unix_attributes = {} + for charm_info in charm_file.infolist(): + # Read input file + with charm_file.open(charm_info) as file: + print(f"charm_info.filename: {charm_info.filename}") + if charm_info.filename == "src/dependency.json": + content = json.loads(file.read()) + # Modify the content of the file by replacing a string + content["snap"]["upgrade_supported"] = "^15" + content["snap"]["version"] = "15.1" + # Write content. + modified_charm_file.writestr(charm_info.filename, json.dumps(content)) + else: # Other file, don't want to modify => just copy it. + content = file.read() + modified_charm_file.writestr(charm_info.filename, content) + unix_attributes[charm_info.filename] = charm_info.external_attr >> 16 + + for modified_charm_info in modified_charm_file.infolist(): + modified_charm_info.external_attr = unix_attributes[modified_charm_info.filename] << 16 + process = subprocess.run( + f"juju refresh --model {ops_test.model.info.name} {app} --path {modified_charm}".split(), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if process.returncode != 0: + raise Exception( + f"Expected juju refresh command to succeed instead it failed: {process.returncode} - {process.stderr.decode()}" + ) async with ops_test.fast_forward(fast_interval="30s"): - await ops_test.model.wait_for_idle(apps=[app], status="active", idle_period=15) + await ops_test.model.wait_for_idle( + apps=[app], status="active", idle_period=15, raise_on_blocked=True + ) # Check whether writes are increasing. logger.info("checking whether writes are increasing") From 4719c1f370b290526fb68305de9d4f35ea2ad5dd Mon Sep 17 00:00:00 2001 From: Marcelo Henrique Neppel Date: Mon, 21 Aug 2023 00:50:30 -0300 Subject: [PATCH 11/13] Add extra tests --- tests/integration/ha_tests/test_upgrade.py | 68 ++++++++++++++++++---- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/tests/integration/ha_tests/test_upgrade.py b/tests/integration/ha_tests/test_upgrade.py index 86ce914a9c..15638e6cb1 100644 --- a/tests/integration/ha_tests/test_upgrade.py +++ b/tests/integration/ha_tests/test_upgrade.py @@ -43,7 +43,7 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: await ops_test.model.wait_for_idle(status="active", timeout=1000) -async def test_upgrade(ops_test: OpsTest, continuous_writes) -> None: +async def test_successful_upgrade(ops_test: OpsTest, continuous_writes) -> None: # Start an application that continuously writes data to the database. logger.info("starting continuous writes to the database") app = await app_name(ops_test) @@ -66,6 +66,40 @@ async def test_upgrade(ops_test: OpsTest, continuous_writes) -> None: await action.wait() assert action.results["Code"] == "0" + # Run juju refresh. + logger.info("refreshing the charm") + application = ops_test.model.applications[app] + charm = await ops_test.build_charm(".") + await application.refresh(path=charm) + async with ops_test.fast_forward(fast_interval="30s"): + await ops_test.model.wait_for_idle( + apps=[app], status="active", idle_period=15, raise_on_blocked=True + ) + + # Check whether writes are increasing. + logger.info("checking whether writes are increasing") + primary_name = await get_primary(ops_test, any_unit_name) + await are_writes_increasing(ops_test, primary_name) + + # Verify that no writes to the database were missed after stopping the writes + # (check that all the units have all the writes). + logger.info("checking whether no writes were lost") + await check_writes(ops_test) + + +async def test_failed_upgrade(ops_test: OpsTest) -> None: + # Run the pre-upgrade-check action. + logger.info("running pre-upgrade check") + app = await app_name(ops_test) + leader_unit_name = None + for unit in ops_test.model.applications[app].units: + if await unit.is_leader_from_status(): + leader_unit_name = unit.name + break + action = await ops_test.model.units.get(leader_unit_name).run_action("pre-upgrade-check") + await action.wait() + assert action.results["Code"] == "0" + # Run juju refresh. logger.info("refreshing the charm") charm = await ops_test.build_charm(".") @@ -106,15 +140,29 @@ async def test_upgrade(ops_test: OpsTest, continuous_writes) -> None: ) async with ops_test.fast_forward(fast_interval="30s"): await ops_test.model.wait_for_idle( - apps=[app], status="active", idle_period=15, raise_on_blocked=True + apps=[app], status="blocked", idle_period=15, raise_on_blocked=False ) - # Check whether writes are increasing. - logger.info("checking whether writes are increasing") - primary_name = await get_primary(ops_test, any_unit_name) - await are_writes_increasing(ops_test, primary_name) - # Verify that no writes to the database were missed after stopping the writes - # (check that all the units have all the writes). - logger.info("checking whether no writes were lost") - await check_writes(ops_test) +async def test_rollback(ops_test: OpsTest) -> None: + # Run the pre-upgrade-check action. + logger.info("running pre-upgrade check") + app = await app_name(ops_test) + leader_unit_name = None + for unit in ops_test.model.applications[app].units: + if await unit.is_leader_from_status(): + leader_unit_name = unit.name + break + action = await ops_test.model.units.get(leader_unit_name).run_action("pre-upgrade-check") + await action.wait() + assert action.results["Code"] == "0" + + # Run juju refresh. + logger.info("refreshing the charm") + application = ops_test.model.applications[app] + charm = await ops_test.build_charm(".") + await application.refresh(path=charm) + async with ops_test.fast_forward(fast_interval="30s"): + await ops_test.model.wait_for_idle( + apps=[app], status="active", idle_period=15, raise_on_blocked=True + ) From 8b60c293f7a399bfb9b53dbb241bd0ed76277848 Mon Sep 17 00:00:00 2001 From: Marcelo Henrique Neppel Date: Thu, 28 Sep 2023 16:35:20 -0300 Subject: [PATCH 12/13] Implement upgrade from stable logic --- src/charm.py | 18 +++++ src/upgrade.py | 72 ++++++++++++++++++- .../ha_tests/test_upgrade_from_stable.py | 0 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 tests/integration/ha_tests/test_upgrade_from_stable.py diff --git a/src/charm.py b/src/charm.py index af41352a14..ccd627befc 100755 --- a/src/charm.py +++ b/src/charm.py @@ -141,6 +141,14 @@ def __init__(self, *args): log_slots=[f"{POSTGRESQL_SNAP_NAME}:logs"], ) + @property + def app_units(self) -> set[Unit]: + """The peer-related units in the application.""" + if not self._peers: + return set() + + return {self.unit, *self._peers.units} + @property def app_peer_data(self) -> Dict: """Application peer relation data object.""" @@ -935,6 +943,12 @@ def _can_start(self, event: StartEvent) -> bool: self._reboot_on_detached_storage(event) return False + # Safeguard against starting while upgrading. + if not self.upgrade.idle: + logger.debug("Defer on_start: Cluster is upgrading") + event.defer() + return False + # Doesn't try to bootstrap the cluster if it's in a blocked state # caused, for example, because a failed installation of packages. if self.is_blocked: @@ -981,6 +995,10 @@ def _setup_exporter(self) -> None: cache = snap.SnapCache() postgres_snap = cache[POSTGRESQL_SNAP_NAME] + if postgres_snap.revision != list(filter(lambda snap_package: snap_package[0] == POSTGRESQL_SNAP_NAME, SNAP_PACKAGES))[0][1]["revision"]: + logger.debug("Early exit _setup_exporter: snap was not refreshed to the right version yet") + return + postgres_snap.set( { "exporter.user": MONITORING_USER, diff --git a/src/upgrade.py b/src/upgrade.py index 1196cd73be..c6ae4d7b66 100644 --- a/src/upgrade.py +++ b/src/upgrade.py @@ -12,12 +12,13 @@ DependencyModel, UpgradeGrantedEvent, ) -from ops.model import ActiveStatus, MaintenanceStatus, WaitingStatus +from ops.model import ActiveStatus, MaintenanceStatus, RelationDataContent, WaitingStatus from pydantic import BaseModel from tenacity import RetryError, Retrying, stop_after_attempt, wait_fixed from typing_extensions import override -from constants import SNAP_PACKAGES +from constants import APP_SCOPE, MONITORING_PASSWORD_KEY, MONITORING_USER, SNAP_PACKAGES +from utils import new_password logger = logging.getLogger(__name__) @@ -43,6 +44,7 @@ def __init__(self, charm, model: BaseModel, **kwargs) -> None: """Initialize the class.""" super().__init__(charm, model, **kwargs) self.charm = charm + self.framework.observe(self.charm.on.upgrade_charm, self._on_upgrade_charm_check_legacy) @override def build_upgrade_stack(self) -> List[int]: @@ -78,6 +80,32 @@ def log_rollback_instructions(self) -> None: "Run `juju refresh --revision postgresql` to initiate the rollback" ) + def _on_upgrade_charm_check_legacy(self, event) -> None: + if not self.peer_relation or len(self.app_units) < len(self.charm.app_units): + # defer case relation not ready or not all units joined it + event.defer() + logger.debug("Wait all units join the upgrade relation") + return + + if self.state: + # Do nothing - if state set, upgrade is supported + return + + if not self.charm.unit.is_leader(): + # set ready state on non-leader units + self.unit_upgrade_data.update({"state": "ready"}) + return + + peers_state = list(filter(lambda state: state != "", self.unit_states)) + + if len(peers_state) == len(self.peer_relation.units) and set(peers_state) == {"ready"}: + # All peers have set the state to ready + self.unit_upgrade_data.update({"state": "ready"}) + self._prepare_upgrade_from_legacy() + else: + logger.debug("Wait until all peers have set upgrade state to ready") + event.defer() + @override def _on_upgrade_granted(self, event: UpgradeGrantedEvent) -> None: # Refresh the charmed PostgreSQL snap and restart the database. @@ -92,6 +120,14 @@ def _on_upgrade_granted(self, event: UpgradeGrantedEvent) -> None: self.charm._setup_exporter() self.charm.backup.start_stop_pgbackrest_service() + try: + self.charm.unit.set_workload_version( + self.charm._patroni.get_postgresql_version() or "unset" + ) + except TypeError: + # Don't fail on this, just log it. + logger.warning("Failed to get PostgreSQL version") + # Wait until the database initialise. self.charm.unit.status = WaitingStatus("waiting for database initialisation") try: @@ -145,3 +181,35 @@ def pre_upgrade_check(self) -> None: "a backup is being created", "wait for the backup creation to finish before starting the upgrade", ) + + def _prepare_upgrade_from_legacy(self) -> None: + """Prepare upgrade from legacy charm without upgrade support. + + Assumes run on leader unit only. + """ + logger.warning("Upgrading from unsupported version") + + # Populate app upgrade databag to allow upgrade procedure + logger.debug("Building upgrade stack") + upgrade_stack = self.build_upgrade_stack() + logger.debug(f"Upgrade stack: {upgrade_stack}") + self.upgrade_stack = upgrade_stack + logger.debug("Persisting dependencies to upgrade relation data...") + self.peer_relation.data[self.charm.app].update( + {"dependencies": json.dumps(self.dependency_model.dict())} + ) + if self.charm.get_secret(APP_SCOPE, MONITORING_PASSWORD_KEY) is None: + self.charm.set_secret(APP_SCOPE, MONITORING_PASSWORD_KEY, new_password()) + users = self.charm.postgresql.list_users() + if MONITORING_USER not in users: + # Create the monitoring user. + self.charm.postgresql.create_user( + MONITORING_USER, + self.charm.get_secret(APP_SCOPE, MONITORING_PASSWORD_KEY), + extra_user_roles="pg_monitor", + ) + + @property + def unit_upgrade_data(self) -> RelationDataContent: + """Return the application upgrade data.""" + return self.peer_relation.data[self.charm.unit] diff --git a/tests/integration/ha_tests/test_upgrade_from_stable.py b/tests/integration/ha_tests/test_upgrade_from_stable.py new file mode 100644 index 0000000000..e69de29bb2 From 539ea200703d75f22e0f3b0934ce29e4bc832829 Mon Sep 17 00:00:00 2001 From: Marcelo Henrique Neppel Date: Fri, 29 Sep 2023 12:22:14 -0300 Subject: [PATCH 13/13] Fix single unit cluster upgrade --- src/upgrade.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/upgrade.py b/src/upgrade.py index c6ae4d7b66..f709ed27b2 100644 --- a/src/upgrade.py +++ b/src/upgrade.py @@ -44,7 +44,9 @@ def __init__(self, charm, model: BaseModel, **kwargs) -> None: """Initialize the class.""" super().__init__(charm, model, **kwargs) self.charm = charm - self.framework.observe(self.charm.on.upgrade_charm, self._on_upgrade_charm_check_legacy) + # self.framework.observe(self.charm.on.upgrade_charm, self._on_upgrade_charm_check_legacy) + logger.error("running") + self._on_upgrade_charm_check_legacy(None) @override def build_upgrade_stack(self) -> List[int]: @@ -82,8 +84,6 @@ def log_rollback_instructions(self) -> None: def _on_upgrade_charm_check_legacy(self, event) -> None: if not self.peer_relation or len(self.app_units) < len(self.charm.app_units): - # defer case relation not ready or not all units joined it - event.defer() logger.debug("Wait all units join the upgrade relation") return @@ -98,13 +98,11 @@ def _on_upgrade_charm_check_legacy(self, event) -> None: peers_state = list(filter(lambda state: state != "", self.unit_states)) - if len(peers_state) == len(self.peer_relation.units) and set(peers_state) == {"ready"}: + if len(peers_state) == len(self.peer_relation.units) and (set(peers_state) == {"ready"} or len(peers_state) == 0): # All peers have set the state to ready self.unit_upgrade_data.update({"state": "ready"}) self._prepare_upgrade_from_legacy() - else: - logger.debug("Wait until all peers have set upgrade state to ready") - event.defer() + getattr(self.on, "upgrade_charm").emit() @override def _on_upgrade_granted(self, event: UpgradeGrantedEvent) -> None: