Skip to content

Commit 3be96f8

Browse files
authored
Merge pull request #4 from canonical/feature/support_legacy_mysql
support for legacy mysql/mariadb
2 parents 6726fb9 + a94a385 commit 3be96f8

File tree

7 files changed

+197
-48
lines changed

7 files changed

+197
-48
lines changed

README.md

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,35 @@
1-
<!--
2-
Avoid using this README file for information that is maintained or published elsewhere, e.g.:
1+
# MySQL Test Application
32

4-
* metadata.yaml > published on Charmhub
5-
* documentation > published on (or linked to from) Charmhub
6-
* detailed contribution guide > documentation or CONTRIBUTING.md
3+
MySQL stack tester charm - this is a simple application used exclusively for integrations test of
4+
[mysql-k8s][mysql-k8s], [mysql][mysql], [mysql-router-k8s][mysql-router-k8s],
5+
[mysql-router][mysql-router], [mysql-bundle-k8s][mysql-bundle-k8s] and
6+
[mysql-bundle][mysql-bundle].
77

8-
Use links instead.
9-
-->
8+
## Relations
109

11-
# mysql-test-app
10+
This charm implements relations interfaces:
11+
* database
12+
* mysql (legacy)
1213

13-
Charmhub package name: operator-template
14-
More information: https://charmhub.io/mysql-test-app
14+
On using the `mysql` legacy relation interface with either [mysql] or [mysql-k8s] charms, its
15+
necessary to config the database name with:
1516

16-
Describe your charm in one or two sentences.
17+
```shell
18+
> juju config mysql-k8s mysql-interface-database=continuous_writes_database
19+
```
1720

18-
## Other resources
21+
## Actions
1922

20-
<!-- If your charm is documented somewhere else other than Charmhub, provide a link separately. -->
23+
Actions are listed on [actions page](https://charmhub.io/mysql-test-app/actions)
2124

22-
- [Read more](https://example.com)
2325

24-
- [Contributing](CONTRIBUTING.md) <!-- or link to other contribution documentation -->
26+
[mysql-k8s]: https://charmhub.io/mysql-k8s
27+
[mysql]: https://charmhub.io/mysql
28+
[mysql-router-k8s]: https://charmhub.io/mysql-router-k8s
29+
[mysql-router]: https://charmhub.io/mysql-router?channel=dpe/edge
30+
[mysql-bundle-k8s]: https://charmhub.io/mysql-bundle-k8s
31+
[mysql-bundle]: https://charmhub.io/mysql-bundle
2532

26-
- See the [Juju SDK documentation](https://juju.is/docs/sdk) for more information about developing and improving charms.
33+
## References
34+
35+
* [MySQL Test App at Charmhub](https://charmhub.io/mysql-test-app)

actions.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ get-session-ssl-cipher:
2727
get-server-certificate:
2828
description: Retrieve server certificate in pem format
2929

30+
get-legacy-mysql-credentials:
31+
description: Get the credentials for the legacy mysql user

metadata.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
name: mysql-test-app
55
description: |
6-
An application charm used in high availability MySQL k8s integration tests.
6+
An application charm used in high availability MySQL k8s/vm integration tests.
77
summary: |
88
Data platform libs application meant to be used
99
only for testing high availability of the MySQL charm.
@@ -12,6 +12,8 @@ requires:
1212
database:
1313
interface: mysql_client
1414
limit: 1
15+
mysql:
16+
interface: mysql
1517

1618
peers:
1719
application-peers:

src/charm.py

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,20 @@
2222
from tenacity import RetryError, Retrying, stop_after_delay, wait_fixed
2323

2424
from connector import MySQLConnector # isort: skip
25+
from literals import (
26+
CONTINUOUS_WRITE_TABLE_NAME,
27+
DATABASE_NAME,
28+
DATABASE_RELATION,
29+
LEGACY_MYSQL_RELATION,
30+
PEER,
31+
PROC_PID_KEY,
32+
RANDOM_VALUE_KEY,
33+
RANDOM_VALUE_TABLE_NAME,
34+
)
35+
from relations.legacy_mysql import LegacyMySQL
2536

2637
logger = logging.getLogger(__name__)
2738

28-
CONTINUOUS_WRITE_TABLE_NAME = "data"
29-
DATABASE_NAME = "continuous_writes_database"
30-
DATABASE_RELATION = "database"
31-
PEER = "application-peers"
32-
PROC_PID_KEY = "proc-pid"
33-
RANDOM_VALUE_KEY = "inserted_value"
34-
RANDOM_VALUE_TABLE_NAME = "random_data"
35-
3639

3740
class MySQLTestApplication(CharmBase):
3841
"""Application charm that continuously writes to MySQL."""
@@ -42,7 +45,9 @@ def __init__(self, *args):
4245

4346
# Charm events
4447
self.framework.observe(self.on.start, self._on_start)
48+
self.framework.observe(self.on[PEER].relation_changed, self._on_peer_relation_changed)
4549

50+
# Action handlers
4651
self.framework.observe(
4752
getattr(self.on, "clear_continuous_writes_action"),
4853
self._on_clear_continuous_writes_action,
@@ -79,6 +84,11 @@ def __init__(self, *args):
7984
self.framework.observe(
8085
self.on[DATABASE_RELATION].relation_broken, self._on_relation_broken
8186
)
87+
self.framework.observe(
88+
self.on[LEGACY_MYSQL_RELATION].relation_broken, self._on_relation_broken
89+
)
90+
# Legacy MySQL/MariaDB Handler
91+
self.legacy_mysql = LegacyMySQL(self)
8292

8393
# ==============
8494
# Properties
@@ -108,13 +118,22 @@ def unit_peer_data(self) -> Dict:
108118
@property
109119
def _database_config(self):
110120
"""Returns the database config to use to connect to the MySQL cluster."""
111-
data = list(self.database.fetch_relation_data().values())[0]
112-
113-
username, password, endpoints = (
114-
data.get("username"),
115-
data.get("password"),
116-
data.get("endpoints"),
117-
)
121+
# identify the database relation
122+
if self.model.get_relation(DATABASE_RELATION):
123+
data = list(self.database.fetch_relation_data().values())[0]
124+
125+
username, password, endpoints = (
126+
data.get("username"),
127+
data.get("password"),
128+
data.get("endpoints"),
129+
)
130+
elif self.model.get_relation(LEGACY_MYSQL_RELATION):
131+
username = self.app_peer_data.get(f"{LEGACY_MYSQL_RELATION}-user")
132+
password = self.app_peer_data.get(f"{LEGACY_MYSQL_RELATION}-password")
133+
endpoints = self.app_peer_data.get(f"{LEGACY_MYSQL_RELATION}-host")
134+
endpoints = f"{endpoints}:3306"
135+
else:
136+
return {}
118137
if None in [username, password, endpoints]:
119138
return {}
120139

@@ -168,9 +187,6 @@ def _start_continuous_writes(self, starting_number: int) -> None:
168187

169188
def _stop_continuous_writes(self) -> Optional[int]:
170189
"""Stop continuous writes to the MySQL cluster and return the last written value."""
171-
if not self._database_config:
172-
return None
173-
174190
if not self.unit_peer_data.get(PROC_PID_KEY):
175191
return None
176192

@@ -193,7 +209,7 @@ def _stop_continuous_writes(self) -> Optional[int]:
193209
return last_written_value
194210

195211
def _max_written_value(self) -> int:
196-
"""Return the count of rows in the continuous writes table."""
212+
"""Return the max value in the continuous writes table."""
197213
if not self._database_config:
198214
return -1
199215

@@ -203,7 +219,7 @@ def _max_written_value(self) -> int:
203219
)
204220
return cursor.fetchone()[0]
205221

206-
def _create_test_table(self, cursor) -> None:
222+
def _create_random_value_table(self, cursor) -> None:
207223
"""Create a test table in the database."""
208224
cursor.execute(
209225
(
@@ -214,7 +230,7 @@ def _create_test_table(self, cursor) -> None:
214230
)
215231
)
216232

217-
def _insert_test_data(self, cursor, random_value: str) -> None:
233+
def _insert_random_value(self, cursor, random_value: str) -> None:
218234
"""Insert the provided random value into the test table in the database."""
219235
cursor.execute(f"INSERT INTO `{RANDOM_VALUE_TABLE_NAME}`(data) VALUES('{random_value}')")
220236

@@ -232,9 +248,9 @@ def _write_random_value(self) -> str:
232248
for attempt in Retrying(stop=stop_after_delay(60), wait=wait_fixed(5)):
233249
with attempt:
234250
with MySQLConnector(self._database_config) as cursor:
235-
self._create_test_table(cursor)
251+
self._create_random_value_table(cursor)
236252
random_value = self._generate_random_values(10)
237-
self._insert_test_data(cursor, random_value)
253+
self._insert_random_value(cursor, random_value)
238254
except RetryError:
239255
logger.exception("Unable to write to the database")
240256
return random_value
@@ -248,8 +264,11 @@ def _write_random_value(self) -> str:
248264
# ==============
249265
def _on_start(self, _) -> None:
250266
"""Handle the start event."""
251-
self.unit.set_workload_version("0.0.1")
252-
self.unit.status = WaitingStatus()
267+
self.unit.set_workload_version("0.0.2")
268+
if self._database_config:
269+
self.unit.status = ActiveStatus()
270+
else:
271+
self.unit.status = WaitingStatus()
253272

254273
def _on_clear_continuous_writes_action(self, _) -> None:
255274
"""Handle the clear continuous writes action event."""
@@ -281,20 +300,32 @@ def _on_database_created(self, _) -> None:
281300
"""Handle the database created event."""
282301
if not self._database_config:
283302
return
284-
285-
self._start_continuous_writes(1)
286-
value = self._write_random_value()
287303
if self.unit.is_leader():
288-
self.app_peer_data[RANDOM_VALUE_KEY] = value
289-
self.unit.status = ActiveStatus()
304+
self.app_peer_data["database-start"] = "true"
290305

291306
def _on_endpoints_changed(self, _) -> None:
292307
"""Handle the database endpoints changed event."""
293308
count = self._max_written_value()
294309
self._start_continuous_writes(count + 1)
295310

311+
def _on_peer_relation_changed(self, _) -> None:
312+
"""Handle common post database estabilshed tasks."""
313+
if self.app_peer_data.get("database-start") == "true":
314+
self._start_continuous_writes(1)
315+
316+
if self.unit.is_leader():
317+
value = self._write_random_value()
318+
self.app_peer_data[RANDOM_VALUE_KEY] = value
319+
# flag should be picked up just once
320+
self.app_peer_data["database-start"] = "done"
321+
322+
self.unit.status = ActiveStatus()
323+
296324
def _on_relation_broken(self, _) -> None:
297325
"""Handle the database relation broken event."""
326+
self._stop_continuous_writes()
327+
if self.unit.is_leader():
328+
self.app_peer_data.pop("database-start", None)
298329
self.unit.status = WaitingStatus()
299330

300331
def _get_inserted_data(self, event: ActionEvent) -> None:

src/continuous_writes.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ def continuous_writes(database_config: Dict, table_name: str, starting_number: i
2222
try:
2323
with MySQLConnector(database_config) as cursor:
2424
cursor.execute(
25-
f"CREATE TABLE IF NOT EXISTS `{table_name}`(number INTEGER, PRIMARY KEY(number));"
25+
(
26+
f"CREATE TABLE IF NOT EXISTS `{table_name}`("
27+
"id INTEGER NOT NULL AUTO_INCREMENT,"
28+
"number INTEGER, "
29+
"PRIMARY KEY(id));"
30+
)
2631
)
2732
except Exception:
2833
pass

src/literals.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright 2023 Canonical Ltd.
2+
# See LICENSE file for licensing details.
3+
4+
"""Test application literals."""
5+
6+
CONTINUOUS_WRITE_TABLE_NAME = "data"
7+
DATABASE_NAME = "continuous_writes_database"
8+
DATABASE_RELATION = "database"
9+
LEGACY_MYSQL_RELATION = "mysql" # MariaDB legacy relation
10+
PEER = "application-peers"
11+
PROC_PID_KEY = "proc-pid"
12+
RANDOM_VALUE_KEY = "inserted_value"
13+
RANDOM_VALUE_TABLE_NAME = "random_data"

src/relations/legacy_mysql.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Copyright 2023 Canonical Ltd.
2+
# See LICENSE file for licensing details.
3+
4+
"""Module for handling legacy MySQL/MariaDB relations."""
5+
6+
import logging
7+
8+
from literals import DATABASE_NAME, LEGACY_MYSQL_RELATION
9+
from ops.framework import Object
10+
from ops.model import BlockedStatus
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
class LegacyMySQL(Object):
16+
"""Class for handling legacy MySQL/MariaDB relations."""
17+
18+
def __init__(self, charm):
19+
super().__init__(charm, LEGACY_MYSQL_RELATION)
20+
self.charm = charm
21+
22+
self.framework.observe(
23+
charm.on[LEGACY_MYSQL_RELATION].relation_joined, self._on_relation_joined
24+
)
25+
self.framework.observe(
26+
charm.on[LEGACY_MYSQL_RELATION].relation_broken, self._on_relation_broken
27+
)
28+
self.framework.observe(
29+
charm.on.get_legacy_mysql_credentials_action, self._get_legacy_mysql_credentials
30+
)
31+
32+
def _on_relation_joined(self, event):
33+
if not self.charm.unit.is_leader():
34+
# only leader handles the relation data
35+
return
36+
37+
# On legacy MariaDB, the relation data is stored on
38+
# leader unit databag only.
39+
try:
40+
relation_data = event.relation.data[event.unit]
41+
except KeyError:
42+
logger.debug("Relation departed")
43+
return
44+
45+
if "user" not in relation_data:
46+
if f"{LEGACY_MYSQL_RELATION}-user" in self.charm.app_peer_data:
47+
# If user set, relation joined already handled
48+
return
49+
logger.debug("Mysql legacy relation data not ready yet. Deferring event.")
50+
event.defer()
51+
return
52+
53+
database_name = relation_data["database"]
54+
if database_name != DATABASE_NAME:
55+
logger.error(f"Database name must be set to `{DATABASE_NAME}`. Modify the test.")
56+
self.charm.unit.status = BlockedStatus("Wrong database name")
57+
return
58+
59+
# Dump data into peer relation
60+
self.charm.app_peer_data[f"{LEGACY_MYSQL_RELATION}-user"] = relation_data["user"]
61+
self.charm.app_peer_data[f"{LEGACY_MYSQL_RELATION}-password"] = relation_data["password"]
62+
self.charm.app_peer_data[f"{LEGACY_MYSQL_RELATION}-host"] = relation_data["host"]
63+
self.charm.app_peer_data[f"{LEGACY_MYSQL_RELATION}-database"] = database_name
64+
65+
# Set database-start to true to trigger common post relation tasks
66+
self.charm.app_peer_data["database-start"] = "true"
67+
68+
def _on_relation_broken(self, _):
69+
if not self.charm.unit.is_leader():
70+
# only leader handles the relation data
71+
return
72+
# Clear data from peer relation
73+
self.charm.app_peer_data.pop(f"{LEGACY_MYSQL_RELATION}-user", None)
74+
self.charm.app_peer_data.pop(f"{LEGACY_MYSQL_RELATION}-password", None)
75+
self.charm.app_peer_data.pop(f"{LEGACY_MYSQL_RELATION}-host", None)
76+
self.charm.app_peer_data.pop(f"{LEGACY_MYSQL_RELATION}-database", None)
77+
78+
def _get_legacy_mysql_credentials(self, event) -> None:
79+
"""Retrieve legacy mariadb credentials."""
80+
event.set_results(
81+
{
82+
"username": self.charm.app_peer_data[f"{LEGACY_MYSQL_RELATION}-user"],
83+
"password": self.charm.app_peer_data[f"{LEGACY_MYSQL_RELATION}-password"],
84+
"host": self.charm.app_peer_data[f"{LEGACY_MYSQL_RELATION}-host"],
85+
"database": self.charm.app_peer_data[f"{LEGACY_MYSQL_RELATION}-database"],
86+
}
87+
)

0 commit comments

Comments
 (0)