Skip to content

Commit 8706c52

Browse files
[DPE-2290] [DPE-2681] Upgrade from 14/stable and add integration tests (#235)
* Added initial upgrade implementation * Updated the code with the new library * Improved code and added unit tests * Added one more check in unit test * Removed upgrade integration tests * Revert "Removed upgrade integration tests" This reverts commit 339830b. * Added replication health check and snap dependency * Added replication health check and snap dependency * Remove dependencies version hashes * Add logic to update dependencies file * Add extra tests * Implement upgrade from stable logic * Fix single unit cluster upgrade * Remove comment * Remove integration tests * Fix unit tests * Minor fixes * Add database setup call * Initial code for test about upgrade from stable * Add integration tests * Test fixes * Fix upgrade test * Fix stable deployment * Add configuration update on upgrade * Fix path on CI * Fix charm channel check
1 parent 572afce commit 8706c52

File tree

9 files changed

+578
-9
lines changed

9 files changed

+578
-9
lines changed

.github/workflows/ci.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ jobs:
8282
- password-rotation-integration
8383
- plugins-integration
8484
- tls-integration
85+
- upgrade-integration
86+
- upgrade-from-stable-integration
8587
agent-versions:
8688
- "2.9.45" # renovate: latest juju 2
8789
- "3.1.5" # renovate: latest juju 3

src/charm.py

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,14 @@ def __init__(self, *args):
148148
log_slots=[f"{POSTGRESQL_SNAP_NAME}:logs"],
149149
)
150150

151+
@property
152+
def app_units(self) -> set[Unit]:
153+
"""The peer-related units in the application."""
154+
if not self._peers:
155+
return set()
156+
157+
return {self.unit, *self._peers.units}
158+
151159
@property
152160
def app_peer_data(self) -> Dict:
153161
"""Application peer relation data object."""
@@ -946,6 +954,12 @@ def _can_start(self, event: StartEvent) -> bool:
946954
self._reboot_on_detached_storage(event)
947955
return False
948956

957+
# Safeguard against starting while upgrading.
958+
if not self.upgrade.idle:
959+
logger.debug("Defer on_start: Cluster is upgrading")
960+
event.defer()
961+
return False
962+
949963
# Doesn't try to bootstrap the cluster if it's in a blocked state
950964
# caused, for example, because a failed installation of packages.
951965
if self.is_blocked:
@@ -992,6 +1006,17 @@ def _setup_exporter(self) -> None:
9921006
cache = snap.SnapCache()
9931007
postgres_snap = cache[POSTGRESQL_SNAP_NAME]
9941008

1009+
if (
1010+
postgres_snap.revision
1011+
!= list(
1012+
filter(lambda snap_package: snap_package[0] == POSTGRESQL_SNAP_NAME, SNAP_PACKAGES)
1013+
)[0][1]["revision"]
1014+
):
1015+
logger.debug(
1016+
"Early exit _setup_exporter: snap was not refreshed to the right version yet"
1017+
)
1018+
return
1019+
9951020
postgres_snap.set(
9961021
{
9971022
"exporter.user": MONITORING_USER,
@@ -1140,11 +1165,7 @@ def _on_set_password(self, event: ActionEvent) -> None:
11401165

11411166
def _on_update_status(self, _) -> None:
11421167
"""Update the unit status message and users list in the database."""
1143-
if "cluster_initialised" not in self._peers.data[self.app]:
1144-
return
1145-
1146-
if self.is_blocked:
1147-
logger.debug("on_update_status early exit: Unit is in Blocked status")
1168+
if not self._can_run_on_update_status():
11481169
return
11491170

11501171
if "restoring-backup" in self.app_peer_data:
@@ -1182,6 +1203,20 @@ def _on_update_status(self, _) -> None:
11821203
# Restart topology observer if it is gone
11831204
self._observer.start_observer()
11841205

1206+
def _can_run_on_update_status(self) -> bool:
1207+
if "cluster_initialised" not in self._peers.data[self.app]:
1208+
return False
1209+
1210+
if not self.upgrade.idle:
1211+
logger.debug("Early exit on_update_status: upgrade in progress")
1212+
return False
1213+
1214+
if self.is_blocked:
1215+
logger.debug("on_update_status early exit: Unit is in Blocked status")
1216+
return False
1217+
1218+
return True
1219+
11851220
def _handle_processes_failures(self) -> bool:
11861221
"""Handle Patroni and PostgreSQL OS processes failures.
11871222

src/upgrade.py

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@
1212
DependencyModel,
1313
UpgradeGrantedEvent,
1414
)
15-
from ops.model import ActiveStatus, MaintenanceStatus, WaitingStatus
15+
from ops.model import MaintenanceStatus, RelationDataContent, WaitingStatus
1616
from pydantic import BaseModel
1717
from tenacity import RetryError, Retrying, stop_after_attempt, wait_fixed
1818
from typing_extensions import override
1919

20-
from constants import SNAP_PACKAGES
20+
from constants import APP_SCOPE, MONITORING_PASSWORD_KEY, MONITORING_USER, SNAP_PACKAGES
21+
from utils import new_password
2122

2223
logger = logging.getLogger(__name__)
2324

@@ -26,6 +27,7 @@ class PostgreSQLDependencyModel(BaseModel):
2627
"""PostgreSQL dependencies model."""
2728

2829
charm: DependencyModel
30+
snap: DependencyModel
2931

3032

3133
def get_postgresql_dependencies_model() -> PostgreSQLDependencyModel:
@@ -42,6 +44,7 @@ def __init__(self, charm, model: BaseModel, **kwargs) -> None:
4244
"""Initialize the class."""
4345
super().__init__(charm, model, **kwargs)
4446
self.charm = charm
47+
self._on_upgrade_charm_check_legacy()
4548

4649
@override
4750
def build_upgrade_stack(self) -> List[int]:
@@ -77,9 +80,56 @@ def log_rollback_instructions(self) -> None:
7780
"Run `juju refresh --revision <previous-revision> postgresql` to initiate the rollback"
7881
)
7982

83+
def _on_upgrade_charm_check_legacy(self) -> None:
84+
if not self.peer_relation or len(self.app_units) < len(self.charm.app_units):
85+
logger.debug("Wait all units join the upgrade relation")
86+
return
87+
88+
if self.state:
89+
# If state set, upgrade is supported. Just set the snap information
90+
# in the dependencies, as it's missing in the first revisions that
91+
# support upgrades.
92+
dependencies = self.peer_relation.data[self.charm.app].get("dependencies")
93+
if (
94+
self.charm.unit.is_leader()
95+
and dependencies is not None
96+
and "snap" not in json.loads(dependencies)
97+
):
98+
fixed_dependencies = json.loads(dependencies)
99+
fixed_dependencies["snap"] = {
100+
"dependencies": {},
101+
"name": "charmed-postgresql",
102+
"upgrade_supported": "^14",
103+
"version": "14.9",
104+
}
105+
self.peer_relation.data[self.charm.app].update(
106+
{"dependencies": json.dumps(fixed_dependencies)}
107+
)
108+
return
109+
110+
if not self.charm.unit.is_leader():
111+
# set ready state on non-leader units
112+
self.unit_upgrade_data.update({"state": "ready"})
113+
return
114+
115+
peers_state = list(filter(lambda state: state != "", self.unit_states))
116+
117+
if len(peers_state) == len(self.peer_relation.units) and (
118+
set(peers_state) == {"ready"} or len(peers_state) == 0
119+
):
120+
if self.charm._patroni.member_started:
121+
# All peers have set the state to ready
122+
self.unit_upgrade_data.update({"state": "ready"})
123+
self._prepare_upgrade_from_legacy()
124+
getattr(self.on, "upgrade_charm").emit()
125+
80126
@override
81127
def _on_upgrade_granted(self, event: UpgradeGrantedEvent) -> None:
82128
# Refresh the charmed PostgreSQL snap and restart the database.
129+
# Update the configuration.
130+
self.charm.unit.status = MaintenanceStatus("updating configuration")
131+
self.charm.update_config()
132+
83133
self.charm.unit.status = MaintenanceStatus("refreshing the snap")
84134
self.charm._install_snap_packages(packages=SNAP_PACKAGES, refresh=True)
85135

@@ -91,6 +141,14 @@ def _on_upgrade_granted(self, event: UpgradeGrantedEvent) -> None:
91141
self.charm._setup_exporter()
92142
self.charm.backup.start_stop_pgbackrest_service()
93143

144+
try:
145+
self.charm.unit.set_workload_version(
146+
self.charm._patroni.get_postgresql_version() or "unset"
147+
)
148+
except TypeError:
149+
# Don't fail on this, just log it.
150+
logger.warning("Failed to get PostgreSQL version")
151+
94152
# Wait until the database initialise.
95153
self.charm.unit.status = WaitingStatus("waiting for database initialisation")
96154
try:
@@ -110,7 +168,6 @@ def _on_upgrade_granted(self, event: UpgradeGrantedEvent) -> None:
110168
raise Exception()
111169

112170
self.set_unit_completed()
113-
self.charm.unit.status = ActiveStatus()
114171

115172
# Ensures leader gets its own relation-changed when it upgrades
116173
if self.charm.unit.is_leader():
@@ -144,3 +201,36 @@ def pre_upgrade_check(self) -> None:
144201
"a backup is being created",
145202
"wait for the backup creation to finish before starting the upgrade",
146203
)
204+
205+
def _prepare_upgrade_from_legacy(self) -> None:
206+
"""Prepare upgrade from legacy charm without upgrade support.
207+
208+
Assumes run on leader unit only.
209+
"""
210+
logger.warning("Upgrading from unsupported version")
211+
212+
# Populate app upgrade databag to allow upgrade procedure
213+
logger.debug("Building upgrade stack")
214+
upgrade_stack = self.build_upgrade_stack()
215+
logger.debug(f"Upgrade stack: {upgrade_stack}")
216+
self.upgrade_stack = upgrade_stack
217+
logger.debug("Persisting dependencies to upgrade relation data...")
218+
self.peer_relation.data[self.charm.app].update(
219+
{"dependencies": json.dumps(self.dependency_model.dict())}
220+
)
221+
if self.charm.get_secret(APP_SCOPE, MONITORING_PASSWORD_KEY) is None:
222+
self.charm.set_secret(APP_SCOPE, MONITORING_PASSWORD_KEY, new_password())
223+
users = self.charm.postgresql.list_users()
224+
if MONITORING_USER not in users:
225+
# Create the monitoring user.
226+
self.charm.postgresql.create_user(
227+
MONITORING_USER,
228+
self.charm.get_secret(APP_SCOPE, MONITORING_PASSWORD_KEY),
229+
extra_user_roles="pg_monitor",
230+
)
231+
self.charm.postgresql.set_up_database()
232+
233+
@property
234+
def unit_upgrade_data(self) -> RelationDataContent:
235+
"""Return the application upgrade data."""
236+
return self.peer_relation.data[self.charm.unit]

0 commit comments

Comments
 (0)