Skip to content

Commit aa27779

Browse files
authored
[DPE-5372] Add safeguard hooks for upgrades (#327)
1 parent 675e66e commit aa27779

File tree

4 files changed

+148
-2
lines changed

4 files changed

+148
-2
lines changed

src/charm.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
CharmBase,
4444
ConfigChangedEvent,
4545
RelationDepartedEvent,
46+
RelationEvent,
4647
StartEvent,
4748
UpdateStatusEvent,
4849
)
@@ -93,6 +94,7 @@ def __init__(self, *args):
9394
super().__init__(*args)
9495

9596
self.framework.observe(self.on.mongod_pebble_ready, self._on_mongod_pebble_ready)
97+
self.framework.observe(self.on.config_changed, self._on_config_changed)
9698
self.framework.observe(self.on.start, self._on_start)
9799
self.framework.observe(self.on.update_status, self._on_update_status)
98100
self.framework.observe(
@@ -659,25 +661,40 @@ def _on_start(self, event) -> None:
659661
event.defer()
660662
return
661663

662-
def _relation_changes_handler(self, event) -> None:
664+
def _relation_changes_handler(self, event: RelationEvent) -> None:
663665
"""Handles different relation events and updates MongoDB replica set."""
664666
self._connect_mongodb_exporter()
665667
self._connect_pbm_agent()
666668

667-
if type(event) is RelationDepartedEvent:
669+
if isinstance(event, RelationDepartedEvent):
668670
if event.departing_unit.name == self.unit.name:
669671
self.unit_peer_data.setdefault("unit_departed", "True")
670672

671673
if not self.unit.is_leader():
672674
return
673675

676+
if self.upgrade_in_progress:
677+
logger.warning(
678+
"Adding replicas during an upgrade is not supported. The charm may be in a broken, unrecoverable state"
679+
)
680+
event.defer()
681+
return
682+
674683
# Admin password and keyFile should be created before running MongoDB.
675684
# This code runs on leader_elected event before mongod_pebble_ready
676685
self._generate_secrets()
677686

678687
if not self.db_initialised:
679688
return
680689

690+
self._reconcile_mongo_hosts_and_users(event)
691+
692+
def _reconcile_mongo_hosts_and_users(self, event: RelationEvent) -> None:
693+
"""Auxiliary function to reconcile mongo data for relation events.
694+
695+
Args:
696+
event: The relation event
697+
"""
681698
with MongoDBConnection(self.mongodb_config) as mongo:
682699
try:
683700
replset_members = mongo.get_replset_members()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright 2024 Canonical Ltd.
2+
# See LICENSE file for licensing details.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2024 Canonical Ltd.
3+
# See LICENSE file for licensing details.
4+
5+
import pytest
6+
from pytest_operator.plugin import OpsTest
7+
8+
from ..ha_tests.helpers import find_unit
9+
from ..helpers import (
10+
APP_NAME,
11+
check_or_scale_app,
12+
get_app_name,
13+
get_password,
14+
set_password,
15+
)
16+
17+
18+
@pytest.mark.skip("Missing upgrade code for now")
19+
@pytest.mark.group(1)
20+
@pytest.mark.abort_on_fail
21+
async def test_build_and_deploy(ops_test: OpsTest):
22+
app_name = await get_app_name(ops_test)
23+
24+
if app_name:
25+
await check_or_scale_app(ops_test, app_name, required_units=3)
26+
return
27+
28+
app_name = APP_NAME
29+
30+
await ops_test.model.deploy(
31+
app_name,
32+
application_name=app_name,
33+
num_units=3,
34+
series="jammy",
35+
channel="6/edge",
36+
)
37+
await ops_test.model.wait_for_idle(
38+
apps=[app_name], status="active", timeout=1000, idle_period=120
39+
)
40+
41+
42+
@pytest.mark.skip("Missing upgrade code for now")
43+
@pytest.mark.group(1)
44+
@pytest.mark.abort_on_fail
45+
async def test_upgrade_password_change_fail(ops_test: OpsTest):
46+
app_name = await get_app_name(ops_test)
47+
leader_id = await find_unit(ops_test, leader=True, app_name=app_name)
48+
49+
current_password = await get_password(ops_test, leader_id, app_name=app_name)
50+
new_charm = await ops_test.build_charm(".")
51+
await ops_test.model.applications[app_name].refresh(path=new_charm)
52+
results = await set_password(ops_test, leader_id, password="0xdeadbeef", app_name=app_name)
53+
54+
assert results == "Cannot set passwords while an upgrade is in progress."
55+
56+
after_action_password = await get_password(ops_test, leader_id, app_name=app_name)
57+
58+
assert current_password == after_action_password

tests/unit/test_upgrade.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright 2024 Canonical Ltd.
2+
# See LICENSE file for licensing details.
3+
import unittest
4+
from unittest.mock import Mock, PropertyMock, patch
5+
6+
from ops.model import ActiveStatus, Relation
7+
from ops.testing import ActionFailed, Harness
8+
from parameterized import parameterized
9+
10+
from charm import MongoDBCharm
11+
from config import Config
12+
13+
from .helpers import patch_network_get
14+
15+
16+
class TestUpgrades(unittest.TestCase):
17+
@patch_network_get(private_address="1.1.1.1")
18+
def setUp(self, *unused):
19+
self.harness = Harness(MongoDBCharm)
20+
self.addCleanup(self.harness.cleanup)
21+
mongo_resource = {
22+
"registrypath": "mongo:4.4",
23+
}
24+
self.harness.add_oci_resource("mongodb-image", mongo_resource)
25+
self.harness.begin()
26+
self.harness.set_leader(True)
27+
self.peer_rel_id = self.harness.add_relation("database-peers", "mongodb-peers")
28+
29+
@patch("ops.framework.EventBase.defer")
30+
@patch("charm.MongoDBCharm.upgrade_in_progress", new_callable=PropertyMock)
31+
def test_on_config_changed_during_upgrade_fails(self, mock_upgrade, defer):
32+
def is_role_changed_mock(*args):
33+
return True
34+
35+
self.harness.charm.is_role_changed = is_role_changed_mock
36+
37+
mock_upgrade.return_value = True
38+
self.harness.charm.on.config_changed.emit()
39+
40+
defer.assert_called()
41+
42+
@parameterized.expand([("relation_joined"), ("relation_changed")])
43+
@patch("charm.MongoDBCharm._connect_pbm_agent")
44+
@patch("charm.MongoDBCharm._connect_mongodb_exporter")
45+
@patch("ops.framework.EventBase.defer")
46+
@patch("charm.MongoDBCharm.upgrade_in_progress", new_callable=PropertyMock)
47+
def test_on_relation_handler(self, handler, mock_upgrade, defer, *unused):
48+
relation: Relation = self.harness.charm.model.get_relation("database-peers")
49+
mock_upgrade.return_value = True
50+
getattr(self.harness.charm.on[Config.Relations.PEERS], handler).emit(relation)
51+
defer.assert_called()
52+
53+
@patch("charm.MongoDBCharm.upgrade_in_progress", new_callable=PropertyMock)
54+
def test_pass_pre_set_password_check_fails(self, mock_upgrade):
55+
def mock_shard_role(*args):
56+
return args != ("shard",)
57+
58+
mock_pbm_status = Mock(return_value=ActiveStatus())
59+
self.harness.charm.is_role = mock_shard_role
60+
mock_upgrade.return_value = True
61+
self.harness.charm.backups.get_pbm_status = mock_pbm_status
62+
63+
with self.assertRaises(ActionFailed) as action_failed:
64+
self.harness.run_action("set-password")
65+
66+
assert (
67+
action_failed.exception.message
68+
== "Cannot set passwords while an upgrade is in progress."
69+
)

0 commit comments

Comments
 (0)