Merge ~stub/charm-k8s-wordpress:ops-lib-mysql into charm-k8s-wordpress:master

Proposed by Stuart Bishop
Status: Merged
Approved by: Stuart Bishop
Approved revision: 83344902f0fe6568b7d6807bc228fa62b2cd87e6
Merged at revision: ae1c52ddc913b1b380fdee5f017054d1e8fe0aed
Proposed branch: ~stub/charm-k8s-wordpress:ops-lib-mysql
Merge into: charm-k8s-wordpress:master
Diff against target: 239 lines (+32/-167)
4 files modified
dev/null (+0/-166)
requirements.txt (+1/-0)
src/charm.py (+1/-1)
tests/unit/test_charm.py (+30/-0)
Reviewer Review Type Date Requested Status
Tom Haddon Approve
Canonical IS Reviewers Pending
Review via email: mp+396227@code.launchpad.net

Commit message

Switch to ops-lib-mysql

Move the embedded mysql endpoint implementation to an external
library for sharing with other charms.

To post a comment you must log in.
Revision history for this message
Stuart Bishop (stub) wrote :

Also adds a test

Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

Revision history for this message
Stuart Bishop (stub) wrote :
Revision history for this message
Tom Haddon (mthaddon) wrote :

Looks good, with one comment about alphabetising requirements.txt.

review: Approve
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision ae1c52ddc913b1b380fdee5f017054d1e8fe0aed

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/requirements.txt b/requirements.txt
2index 875fb9a..32921cf 100644
3--- a/requirements.txt
4+++ b/requirements.txt
5@@ -1,3 +1,4 @@
6 # Include python requirements here
7 ops
8+ops-lib-mysql
9 requests
10diff --git a/src/charm.py b/src/charm.py
11index f1a2252..990d55d 100755
12--- a/src/charm.py
13+++ b/src/charm.py
14@@ -12,7 +12,7 @@ from ops.main import main
15 from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, WaitingStatus
16 from leadership import LeadershipSettings
17
18-from mysql import MySQLClient
19+from opslib.mysql import MySQLClient
20 from wordpress import Wordpress, password_generator, WORDPRESS_SECRETS
21
22
23diff --git a/src/mysql.py b/src/mysql.py
24deleted file mode 100644
25index 8429b38..0000000
26--- a/src/mysql.py
27+++ /dev/null
28@@ -1,166 +0,0 @@
29-"""
30-MySQL endpoint implementation for the Operator Framework.
31-
32-Ported to the Operator Framework from the canonical-osm Reactive
33-charms at https://git.launchpad.net/canonical-osm
34-"""
35-
36-import logging
37-
38-import ops.charm
39-import ops.framework
40-import ops.model
41-
42-
43-__all__ = ["MySQLClient", "MySQLClientEvents", "MySQLRelationEvent", "MySQLDatabaseChangedEvent"]
44-
45-
46-class _MySQLConnectionDetails(object):
47- database: str = None
48- host: str = None
49- port: int = 3306
50- user: str = None
51- password: str = None
52- root_password: str = None
53- connection_string: str = None
54- sanitized_connection_string: str = None # With no secrets, for logging.
55- is_available: bool = False
56-
57- def __init__(self, relation: ops.model.Relation, unit: ops.model.Unit):
58- reldata = relation.data.get(unit, {})
59- self.database = reldata.get("database", None)
60- self.host = reldata.get("host", None)
61- self.port = int(reldata.get("port", 3306))
62- self.user = reldata.get("user", None)
63- self.password = reldata.get("password", None)
64- self.root_password = reldata.get("root_password", None)
65-
66- if all([self.database, self.host, self.port, self.user, self.password, self.root_password]):
67- self.sanitized_connection_string = (
68- f"host={self.host} port={self.port} dbname={self.database} user={self.user}"
69- )
70- self.connection_string = (
71- self.sanitized_connection_string + f" password={self.password} root_password={self.root_password}"
72- )
73- else:
74- self.sanitized_connection_string = None
75- self.connection_string = None
76- self.is_available = self.connection_string is not None
77-
78-
79-class MySQLRelationEvent(ops.charm.RelationEvent):
80- def __init__(self, *args, **kw):
81- super().__init__(*args, **kw)
82- self._conn = _MySQLConnectionDetails(self.relation, self.unit)
83-
84- @property
85- def is_available(self) -> bool:
86- """True if the database is available for use."""
87- return self._conn.is_available
88-
89- @property
90- def connection_string(self) -> str:
91- """The connection string, if available, or None.
92-
93- The connection string will be in the format:
94-
95- 'host={host} port={port} dbname={database} user={user} password={password} root_password={root_password}'
96- """
97- return self._conn.connection_string
98-
99- @property
100- def database(self) -> str:
101- """The name of the provided database, or None."""
102- return self._conn.database
103-
104- @property
105- def host(self) -> str:
106- """The host for the provided database, or None."""
107- return self._conn.host
108-
109- @property
110- def port(self) -> int:
111- """The port to the provided database."""
112- # If not available, returns the default port of 3306.
113- return self._conn.port
114-
115- @property
116- def user(self) -> str:
117- """The username for the provided database, or None."""
118- return self._conn.user
119-
120- @property
121- def password(self) -> str:
122- """The password for the provided database, or None."""
123- return self._conn.password
124-
125- @property
126- def root_password(self) -> str:
127- """The password for the root user, or None."""
128- return self._conn.root_password
129-
130- def restore(self, snapshot) -> None:
131- super().restore(snapshot)
132- self._conn = _MySQLConnectionDetails(self.relation, self.unit)
133-
134-
135-class MySQLDatabaseChangedEvent(MySQLRelationEvent):
136- """The database connection details on the relation have changed.
137-
138- This event is emitted when the database first becomes available
139- for use, when the connection details have changed, and when it
140- becomes unavailable.
141- """
142-
143- pass
144-
145-
146-class MySQLClientEvents(ops.framework.ObjectEvents):
147- database_changed = ops.framework.EventSource(MySQLDatabaseChangedEvent)
148-
149-
150-class MySQLClient(ops.framework.Object):
151- """Requires side of a MySQL Endpoint"""
152-
153- on = MySQLClientEvents()
154- _state = ops.framework.StoredState()
155-
156- relation_name: str = None
157- log: logging.Logger = None
158-
159- def __init__(self, charm: ops.charm.CharmBase, relation_name: str):
160- super().__init__(charm, relation_name)
161-
162- self.relation_name = relation_name
163- self.log = logging.getLogger("mysql.client.{}".format(relation_name))
164- self._state.set_default(rels={})
165-
166- self.framework.observe(charm.on[relation_name].relation_changed, self._on_changed)
167- self.framework.observe(charm.on[relation_name].relation_broken, self._on_broken)
168-
169- def _on_changed(self, event: ops.charm.RelationEvent) -> None:
170- if event.unit is None:
171- return # Ignore application relation data events.
172-
173- prev_conn_str = self._state.rels.get(event.relation.id, None)
174- new_cd = _MySQLConnectionDetails(event.relation, event.unit)
175- new_conn_str = new_cd.connection_string
176-
177- if prev_conn_str != new_conn_str:
178- self._state.rels[event.relation.id] = new_conn_str
179- if new_conn_str is None:
180- self.log.info(f"Database on relation {event.relation.id} is no longer available.")
181- else:
182- self.log.info(
183- f"Database on relation {event.relation.id} available at {new_cd.sanitized_connection_string}."
184- )
185- self.on.database_changed.emit(relation=event.relation, app=event.app, unit=event.unit)
186-
187- def _on_broken(self, event: ops.charm.RelationEvent) -> None:
188- self.log.info(f"Database relation {event.relation.id} is gone.")
189- prev_conn_str = self._state.rels.get(event.relation.id, None)
190- if event.relation.id in self._state.rels:
191- del self._state.rels[event.relation.id]
192- if prev_conn_str is None:
193- return
194- self.on.database_changed.emit(relation=event.relation, app=event.app, unit=None)
195diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py
196index acf406c..bd4d237 100644
197--- a/tests/unit/test_charm.py
198+++ b/tests/unit/test_charm.py
199@@ -30,10 +30,40 @@ class TestWordpressCharm(unittest.TestCase):
200
201 def setUp(self):
202 self.harness = testing.Harness(WordpressCharm)
203+ self.addCleanup(self.harness.cleanup)
204
205 self.harness.begin()
206 self.harness.update_config(copy.deepcopy(self.test_model_config))
207
208+ def test_db_relation(self):
209+ # Charm starts with no relation, defaulting to using db
210+ # connection details from the charm config.
211+ charm = self.harness.charm
212+ self.assertFalse(charm.state.has_db_relation)
213+ self.assertEqual(charm.state.db_host, TEST_MODEL_CONFIG["db_host"])
214+ self.assertEqual(charm.state.db_name, TEST_MODEL_CONFIG["db_name"])
215+ self.assertEqual(charm.state.db_user, TEST_MODEL_CONFIG["db_user"])
216+ self.assertEqual(charm.state.db_password, TEST_MODEL_CONFIG["db_password"])
217+
218+ # Add a relation and remote unit providing connection details.
219+ # TODO: ops-lib-mysql should have a helper to set the relation data.
220+ relid = self.harness.add_relation("db", "mysql")
221+ self.harness.add_relation_unit(relid, "mysql/0")
222+ self.harness.update_relation_data(relid, "mysql/0", {
223+ "database": "wpdbname",
224+ "host": "hostname.local",
225+ "port": "3306",
226+ "user": "wpuser",
227+ "password": "s3cret",
228+ "root_password": "sup3r_s3cret",
229+ })
230+ # charm.db.on.database_changed fires here and is handled, updating state.
231+ self.assertTrue(charm.state.has_db_relation)
232+ self.assertEqual(charm.state.db_host, "hostname.local")
233+ self.assertEqual(charm.state.db_name, "wpdbname")
234+ self.assertEqual(charm.state.db_user, "wpuser")
235+ self.assertEqual(charm.state.db_password, "s3cret")
236+
237 def test_is_config_valid(self):
238 # Test a valid model config.
239 want_true = self.harness.charm.is_valid_config()

Subscribers

People subscribed via source and target branches