Skip to content

Commit 8e8939f

Browse files
[DPE-7322] Support predefined roles (#635)
1 parent 0f97e21 commit 8e8939f

File tree

19 files changed

+820
-180
lines changed

19 files changed

+820
-180
lines changed

docs/reference/software-testing.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,13 @@ juju run mysql-test-app/leader get-inserted-data
4141
# Start "continuous write" test:
4242
juju run mysql-test-app/leader start-continuous-writes
4343
export password=$(juju run mysql-k8s/leader get-password username=root | yq '.. | select(. | has("password")).password')
44-
watch -n1 -x juju ssh --container mysql mysql-k8s/leader "mysql -h 127.0.0.1 -uroot -p${password} -e \"select count(*) from continuous_writes_database.data\""
44+
watch -n1 -x juju ssh --container mysql mysql-k8s/leader "mysql -h 127.0.0.1 -uroot -p${password} -e \"select count(*) from continuous_writes.data\""
4545

4646
# Watch the counter is growing!
4747
```
4848
Expected results:
4949

50-
* mysql-test-app continuously inserts records in database `continuous_writes_database` table `data`.
50+
* mysql-test-app continuously inserts records in database `continuous_writes` table `data`.
5151
* the counters (amount of records in table) are growing on all cluster members
5252

5353
Hints:

lib/charms/mysql/v0/mysql.py

Lines changed: 219 additions & 95 deletions
Large diffs are not rendered by default.

src/charm.py

Lines changed: 26 additions & 30 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,
@@ -373,6 +375,25 @@ def is_unit_busy(self) -> bool:
373375
"""Returns whether the unit is busy."""
374376
return self._is_cluster_blocked()
375377

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

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

728738
logger.info("Configuring initialized mysqld")
729739
# Configure all base users and revoke privileges from the root users
730-
self._mysql.configure_mysql_users()
740+
self._mysql.configure_mysql_router_roles()
741+
self._mysql.configure_mysql_system_roles()
742+
self._mysql.configure_mysql_system_users()
731743

732744
if self.config.plugin_audit_enabled:
733745
# Enable the audit plugin
@@ -739,6 +751,7 @@ def _configure_instance(self, container) -> None:
739751
except (
740752
MySQLInitialiseMySQLDError,
741753
MySQLServiceNotRunningError,
754+
MySQLConfigureMySQLRolesError,
742755
MySQLConfigureMySQLUsersError,
743756
MySQLConfigureInstanceError,
744757
ChangeError,
@@ -760,8 +773,6 @@ def _configure_instance(self, container) -> None:
760773
else:
761774
container.start(MYSQLD_EXPORTER_SERVICE)
762775

763-
self._open_ports()
764-
765776
try:
766777
# Set workload version
767778
if workload_version := self._mysql.get_mysql_version():
@@ -842,22 +853,7 @@ def _on_mysql_pebble_ready(self, event) -> None:
842853
self.join_unit_to_cluster()
843854
return
844855

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

863859
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
@@ -449,7 +449,7 @@ def _wait_until_unit_removed_from_cluster(self, unit_address: str) -> None:
449449
if unit_address in members_in_cluster:
450450
raise MySQLWaitUntilUnitRemovedFromClusterError("Remove member still in cluster")
451451

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

477-
def create_user(self, username: str, password: str, label: str, hostname: str = "%") -> None:
477+
def create_user_legacy(
478+
self, username: str, password: str, label: str, hostname: str = "%"
479+
) -> None:
478480
"""Creates a new user.
479481
480482
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
@@ -246,14 +247,18 @@ def _on_mysql_relation_created(self, event: RelationCreatedEvent) -> None:
246247

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

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
@@ -107,16 +109,17 @@ def _on_database_requested(self, event: DatabaseRequestedEvent) -> None:
107109

108110
# get base relation data
109111
relation_id = event.relation.id
112+
app_name = event.app.name
110113
db_name = event.database
114+
111115
extra_user_roles = []
112116
if event.extra_user_roles:
113117
extra_user_roles = event.extra_user_roles.split(",")
118+
114119
# user name is derived from the relation id
115120
db_user = self._get_username(relation_id)
116121
db_pass = self._get_or_set_password(event.relation)
117122

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

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

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

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

src/relations/mysql_root.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,12 +203,13 @@ def _on_mysql_root_relation_created(self, event: RelationCreatedEvent) -> None:
203203
root_password = self.charm.get_secret("app", ROOT_PASSWORD_KEY)
204204
if not root_password:
205205
raise MySQLCreateUserError("MySQL root password not found in peer secrets")
206-
self.charm._mysql.create_database(database)
207-
self.charm._mysql.create_user(username, password, "mysql-root-legacy-relation")
206+
207+
self.charm._mysql.create_database_legacy(database)
208+
self.charm._mysql.create_user_legacy(username, password, "mysql-root-legacy-relation")
208209
if not self.charm._mysql.does_mysql_user_exist("root", "%"):
209210
# create `root@%` user if it doesn't exist
210211
# this is needed for the `mysql-root` interface to work
211-
self.charm._mysql.create_user(
212+
self.charm._mysql.create_user_legacy(
212213
"root",
213214
root_password,
214215
"mysql-root-legacy-relation",

tests/integration/high_availability/high_availability_helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
)
3333

3434
# Copied these values from high_availability.application_charm.src.charm
35-
DATABASE_NAME = "continuous_writes_database"
35+
DATABASE_NAME = "continuous_writes"
3636
TABLE_NAME = "data"
3737

3838
CLUSTER_NAME = "test_cluster"

tests/integration/relations/test_mysql_root.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ async def test_build_and_deploy(ops_test: OpsTest, charm):
2929
config = {
3030
"profile": "testing",
3131
"mysql-root-interface-user": "test-user",
32-
"mysql-root-interface-database": "continuous_writes_database",
32+
"mysql-root-interface-database": "continuous_writes",
3333
}
3434
resources = {
3535
"mysql-image": DB_METADATA["resources"]["mysql-image"]["upstream-source"],

tests/integration/roles/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright 2025 Canonical Ltd.
2+
# See LICENSE file for licensing details.

0 commit comments

Comments
 (0)