Skip to content

Commit c8c1ee4

Browse files
[DRAFT]
1 parent 49b09f9 commit c8c1ee4

File tree

13 files changed

+639
-171
lines changed

13 files changed

+639
-171
lines changed

lib/charms/mysql/v0/mysql.py

Lines changed: 202 additions & 89 deletions
Large diffs are not rendered by default.

src/charm.py

Lines changed: 26 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@
3434
MySQLAddInstanceToClusterError,
3535
MySQLCharmBase,
3636
MySQLConfigureInstanceError,
37+
MySQLConfigureMySQLRolesError,
3738
MySQLConfigureMySQLUsersError,
3839
MySQLCreateClusterError,
40+
MySQLCreateClusterSetError,
3941
MySQLGetClusterPrimaryAddressError,
4042
MySQLGetMySQLVersionError,
4143
MySQLInitializeJujuOperationsTableError,
@@ -372,6 +374,25 @@ def is_unit_busy(self) -> bool:
372374
"""Returns whether the unit is busy."""
373375
return self._is_cluster_blocked()
374376

377+
def _create_cluster(self) -> None:
378+
juju_version = ops.JujuVersion.from_environ()
379+
380+
try:
381+
# Create the cluster when is the leader unit
382+
logger.info(f"Creating cluster {self.app_peer_data['cluster-name']}")
383+
self.create_cluster()
384+
self.unit.set_ports(3306, 33060) if juju_version.supports_open_port_on_k8s else None
385+
self.unit.status = ops.ActiveStatus(self.active_status_message)
386+
except (
387+
MySQLCreateClusterError,
388+
MySQLCreateClusterSetError,
389+
MySQLInitializeJujuOperationsTableError,
390+
MySQLNoMemberStateError,
391+
MySQLUnableToGetMemberStateError,
392+
):
393+
logger.exception("Failed to initialize primary")
394+
raise
395+
375396
def _get_primary_from_online_peer(self) -> Optional[str]:
376397
"""Get the primary address from an online peer."""
377398
for unit in self.peers.units:
@@ -677,17 +698,6 @@ def _on_leader_elected(self, _) -> None:
677698
"cluster-set-domain-name", self.config.cluster_set_name or f"cluster-set-{common_hash}"
678699
)
679700

680-
def _open_ports(self) -> None:
681-
"""Open ports if supported.
682-
683-
Used if `juju expose` ran on application
684-
"""
685-
if ops.JujuVersion.from_environ().supports_open_port_on_k8s:
686-
try:
687-
self.unit.set_ports(3306, 33060)
688-
except ops.ModelError:
689-
logger.exception("failed to open port")
690-
691701
def _write_mysqld_configuration(self) -> dict:
692702
"""Write the mysqld configuration to the file."""
693703
memory_limit_bytes = (self.config.profile_limit_memory or 0) * BYTES_1MB
@@ -725,7 +735,9 @@ def _configure_instance(self, container) -> None:
725735

726736
logger.info("Configuring initialized mysqld")
727737
# Configure all base users and revoke privileges from the root users
728-
self._mysql.configure_mysql_users()
738+
self._mysql.configure_mysql_router_roles()
739+
self._mysql.configure_mysql_system_roles()
740+
self._mysql.configure_mysql_system_users()
729741

730742
if self.config.plugin_audit_enabled:
731743
# Enable the audit plugin
@@ -737,6 +749,7 @@ def _configure_instance(self, container) -> None:
737749
except (
738750
MySQLInitialiseMySQLDError,
739751
MySQLServiceNotRunningError,
752+
MySQLConfigureMySQLRolesError,
740753
MySQLConfigureMySQLUsersError,
741754
MySQLConfigureInstanceError,
742755
ChangeError,
@@ -758,8 +771,6 @@ def _configure_instance(self, container) -> None:
758771
else:
759772
container.start(MYSQLD_EXPORTER_SERVICE)
760773

761-
self._open_ports()
762-
763774
try:
764775
# Set workload version
765776
if workload_version := self._mysql.get_mysql_version():
@@ -840,23 +851,7 @@ def _on_mysql_pebble_ready(self, event) -> None:
840851
self.join_unit_to_cluster()
841852
return
842853

843-
try:
844-
# Create the cluster when is the leader unit
845-
logger.info(f"Creating cluster {self.app_peer_data['cluster-name']}")
846-
self.unit.status = MaintenanceStatus("Creating cluster")
847-
self.create_cluster()
848-
self.unit.status = ops.ActiveStatus(self.active_status_message)
849-
850-
except (
851-
MySQLCreateClusterError,
852-
MySQLUnableToGetMemberStateError,
853-
MySQLNoMemberStateError,
854-
MySQLInitializeJujuOperationsTableError,
855-
MySQLCreateClusterError,
856-
):
857-
logger.exception("Failed to initialize primary")
858-
raise
859-
854+
self._create_cluster()
860855
self._mysql.reconcile_binlogs_collection(force_restart=True)
861856

862857
def _handle_potential_cluster_crash_scenario(self) -> bool: # noqa: C901

src/mysql_k8s_helpers.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ def _wait_until_unit_removed_from_cluster(self, unit_address: str) -> None:
450450
if unit_address in members_in_cluster:
451451
raise MySQLWaitUntilUnitRemovedFromClusterError("Remove member still in cluster")
452452

453-
def create_database(self, database_name: str) -> None:
453+
def create_database_legacy(self, database_name: str) -> None:
454454
"""Creates a database.
455455
456456
Args:
@@ -475,7 +475,9 @@ def create_database(self, database_name: str) -> None:
475475
logger.exception(f"Failed to create database {database_name}", exc_info=e)
476476
raise MySQLCreateDatabaseError(e.message)
477477

478-
def create_user(self, username: str, password: str, label: str, hostname: str = "%") -> None:
478+
def create_user_legacy(
479+
self, username: str, password: str, label: str, hostname: str = "%"
480+
) -> None:
479481
"""Creates a new user.
480482
481483
Args:

src/relations/mysql.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99

1010
from charms.mysql.v0.mysql import (
1111
MySQLCheckUserExistenceError,
12-
MySQLCreateApplicationDatabaseAndScopedUserError,
12+
MySQLCreateApplicationDatabaseError,
13+
MySQLCreateApplicationScopedUserError,
1314
MySQLDeleteUsersForUnitError,
1415
)
1516
from ops.charm import RelationBrokenEvent, RelationChangedEvent, RelationCreatedEvent
@@ -245,14 +246,18 @@ def _on_mysql_relation_created(self, event: RelationCreatedEvent) -> None: # no
245246

246247
try:
247248
logger.info("Creating application database and scoped user")
248-
self.charm._mysql.create_application_database_and_scoped_user(
249+
self.charm._mysql.create_database(database)
250+
self.charm._mysql.create_scoped_user(
249251
database,
250252
username,
251253
password,
252254
"%",
253255
unit_name="mysql-legacy-relation",
254256
)
255-
except MySQLCreateApplicationDatabaseAndScopedUserError:
257+
except (
258+
MySQLCreateApplicationDatabaseError,
259+
MySQLCreateApplicationScopedUserError,
260+
):
256261
self.charm.unit.status = BlockedStatus(
257262
"Failed to create application database and scoped user"
258263
)

src/relations/mysql_provider.py

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99

1010
from charms.data_platform_libs.v0.data_interfaces import DatabaseProvides, DatabaseRequestedEvent
1111
from charms.mysql.v0.mysql import (
12-
MySQLCreateApplicationDatabaseAndScopedUserError,
12+
LEGACY_ROLE_ROUTER,
13+
MODERN_ROLE_ROUTER,
14+
MySQLCreateApplicationDatabaseError,
15+
MySQLCreateApplicationScopedUserError,
1316
MySQLDeleteUserError,
1417
MySQLDeleteUsersForRelationError,
1518
MySQLGetMySQLVersionError,
16-
MySQLGrantPrivilegesToUserError,
1719
MySQLRemoveRouterFromMetadataError,
1820
)
1921
from ops.charm import PebbleReadyEvent, RelationBrokenEvent, RelationDepartedEvent
@@ -108,16 +110,17 @@ def _on_database_requested(self, event: DatabaseRequestedEvent) -> None:
108110

109111
# get base relation data
110112
relation_id = event.relation.id
113+
app_name = event.app.name
111114
db_name = event.database
115+
112116
extra_user_roles = []
113117
if event.extra_user_roles:
114118
extra_user_roles = event.extra_user_roles.split(",")
119+
115120
# user name is derived from the relation id
116121
db_user = self._get_username(relation_id)
117122
db_pass = self._get_or_set_password(event.relation)
118123

119-
remote_app = event.app.name
120-
121124
try:
122125
# make sure pods are labeled before adding service
123126
self.charm._mysql.update_endpoints(DB_RELATION_NAME)
@@ -133,25 +136,19 @@ def _on_database_requested(self, event: DatabaseRequestedEvent) -> None:
133136
# wait for endpoints to be ready
134137
self.charm.k8s_helpers.wait_service_ready((primary_endpoint, 3306))
135138

136-
if "mysqlrouter" in extra_user_roles:
137-
self.charm._mysql.create_application_database_and_scoped_user(
138-
db_name,
139-
db_user,
140-
db_pass,
141-
"%",
142-
# MySQL Router charm does not need a new database
143-
create_database=False,
144-
)
145-
self.charm._mysql.grant_privileges_to_user(
146-
db_user, "%", ["ALL PRIVILEGES"], with_grant_option=True
147-
)
148-
else:
149-
# TODO:
150-
# add setup of tls, tls_ca and status
151-
# add extra roles parsing from relation data
152-
self.charm._mysql.create_application_database_and_scoped_user(
153-
db_name, db_user, db_pass, "%"
154-
)
139+
if not any([
140+
LEGACY_ROLE_ROUTER in extra_user_roles,
141+
MODERN_ROLE_ROUTER in extra_user_roles,
142+
]):
143+
self.charm._mysql.create_database(db_name)
144+
145+
self.charm._mysql.create_scoped_user(
146+
db_name,
147+
db_user,
148+
db_pass,
149+
"%",
150+
extra_roles=extra_user_roles,
151+
)
155152

156153
# Set relation data
157154
self.database.set_endpoints(relation_id, f"{primary_endpoint}:3306")
@@ -160,11 +157,12 @@ def _on_database_requested(self, event: DatabaseRequestedEvent) -> None:
160157
self.database.set_version(relation_id, db_version)
161158
self.database.set_database(relation_id, db_name)
162159

163-
logger.info(f"Created user for app {remote_app}")
160+
logger.info(f"Created user for app {app_name}")
161+
self.charm.unit.status = ActiveStatus()
164162
except (
165-
MySQLCreateApplicationDatabaseAndScopedUserError,
163+
MySQLCreateApplicationDatabaseError,
164+
MySQLCreateApplicationScopedUserError,
166165
MySQLGetMySQLVersionError,
167-
MySQLGrantPrivilegesToUserError,
168166
) as e:
169167
logger.exception("Failed to set up database relation", exc_info=e)
170168
self.charm.unit.status = BlockedStatus("Failed to create scoped user")

src/relations/mysql_root.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,12 +190,12 @@ def _on_mysql_root_relation_created(self, event: RelationCreatedEvent) -> None:
190190
try:
191191
root_password = self.charm.get_secret("app", ROOT_PASSWORD_KEY)
192192
assert root_password, "Root password not set"
193-
self.charm._mysql.create_database(database)
194-
self.charm._mysql.create_user(username, password, "mysql-root-legacy-relation")
193+
self.charm._mysql.create_database_legacy(database)
194+
self.charm._mysql.create_user_legacy(username, password, "mysql-root-legacy-relation")
195195
if not self.charm._mysql.does_mysql_user_exist("root", "%"):
196196
# create `root@%` user if it doesn't exist
197197
# this is needed for the `mysql-root` interface to work
198-
self.charm._mysql.create_user(
198+
self.charm._mysql.create_user_legacy(
199199
"root",
200200
root_password,
201201
"mysql-root-legacy-relation",
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2025 Canonical Ltd.
3+
# See LICENSE file for licensing details.
4+
5+
import asyncio
6+
from pathlib import Path
7+
8+
import pytest
9+
import yaml
10+
from pytest_operator.plugin import OpsTest
11+
12+
from . import juju_
13+
from .helpers import (
14+
execute_queries_on_unit,
15+
get_primary_unit,
16+
get_server_config_credentials,
17+
get_unit_address,
18+
)
19+
20+
METADATA = yaml.safe_load(Path("./metadata.yaml").read_text())
21+
22+
DATABASE_APP_NAME = METADATA["name"]
23+
INTEGRATOR_APP_NAME = "data-integrator"
24+
25+
26+
@pytest.mark.abort_on_fail
27+
async def test_build_and_deploy(ops_test: OpsTest, charm) -> None:
28+
"""Simple test to ensure that the mysql and data-integrator charms get deployed."""
29+
resources = {"mysql-image": METADATA["resources"]["mysql-image"]["upstream-source"]}
30+
31+
async with ops_test.fast_forward("10s"):
32+
await asyncio.gather(
33+
ops_test.model.deploy(
34+
charm,
35+
application_name=DATABASE_APP_NAME,
36+
num_units=3,
37+
resources=resources,
38+
39+
config={"profile": "testing"},
40+
),
41+
ops_test.model.deploy(
42+
INTEGRATOR_APP_NAME,
43+
44+
),
45+
)
46+
47+
await ops_test.model.wait_for_idle(apps=[DATABASE_APP_NAME], status="active")
48+
await ops_test.model.wait_for_idle(apps=[INTEGRATOR_APP_NAME], status="blocked")
49+
50+
51+
@pytest.mark.abort_on_fail
52+
async def test_charmed_dba_role(ops_test: OpsTest):
53+
"""Test the DBA predefined role."""
54+
await ops_test.model.applications[INTEGRATOR_APP_NAME].set_config({
55+
"database-name": "charmed_dba_database",
56+
"extra-user-roles": "charmed_dba",
57+
})
58+
await ops_test.model.add_relation(INTEGRATOR_APP_NAME, DATABASE_APP_NAME)
59+
await ops_test.model.wait_for_idle(
60+
apps=[INTEGRATOR_APP_NAME, DATABASE_APP_NAME], status="active"
61+
)
62+
63+
mysql_unit = ops_test.model.applications[DATABASE_APP_NAME].units[0]
64+
primary_unit = await get_primary_unit(ops_test, mysql_unit, DATABASE_APP_NAME)
65+
primary_unit_address = await get_unit_address(ops_test, primary_unit.name)
66+
server_config_credentials = await get_server_config_credentials(primary_unit)
67+
68+
execute_queries_on_unit(
69+
primary_unit_address,
70+
server_config_credentials["username"],
71+
server_config_credentials["password"],
72+
["CREATE DATABASE IF NOT EXISTS test"],
73+
commit=True,
74+
)
75+
76+
data_integrator_unit = ops_test.model.applications[INTEGRATOR_APP_NAME].units[0]
77+
results = await juju_.run_action(data_integrator_unit, "get-credentials")
78+
79+
rows = execute_queries_on_unit(
80+
primary_unit_address,
81+
results["mysql"]["username"],
82+
results["mysql"]["password"],
83+
["SHOW DATABASES"],
84+
commit=True,
85+
)
86+
87+
assert "test" in rows, "Database is not visible to DBA user"

0 commit comments

Comments
 (0)