Skip to content

[DPE-7322] Support predefined roles #635

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
310 changes: 220 additions & 90 deletions lib/charms/mysql/v0/mysql.py

Large diffs are not rendered by default.

57 changes: 26 additions & 31 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@
MySQLAddInstanceToClusterError,
MySQLCharmBase,
MySQLConfigureInstanceError,
MySQLConfigureMySQLRolesError,
MySQLConfigureMySQLUsersError,
MySQLCreateClusterError,
MySQLCreateClusterSetError,
MySQLGetClusterPrimaryAddressError,
MySQLGetMySQLVersionError,
MySQLInitializeJujuOperationsTableError,
Expand Down Expand Up @@ -372,6 +374,25 @@ def is_unit_busy(self) -> bool:
"""Returns whether the unit is busy."""
return self._is_cluster_blocked()

def _create_cluster(self) -> None:
juju_version = ops.JujuVersion.from_environ()

try:
# Create the cluster when is the leader unit
logger.info(f"Creating cluster {self.app_peer_data['cluster-name']}")
self.create_cluster()
self.unit.set_ports(3306, 33060) if juju_version.supports_open_port_on_k8s else None
self.unit.status = ops.ActiveStatus(self.active_status_message)
except (
MySQLCreateClusterError,
MySQLCreateClusterSetError,
MySQLInitializeJujuOperationsTableError,
MySQLNoMemberStateError,
MySQLUnableToGetMemberStateError,
):
logger.exception("Failed to initialize primary")
raise

def _get_primary_from_online_peer(self) -> Optional[str]:
"""Get the primary address from an online peer."""
for unit in self.peers.units:
Expand Down Expand Up @@ -677,17 +698,6 @@ def _on_leader_elected(self, _) -> None:
"cluster-set-domain-name", self.config.cluster_set_name or f"cluster-set-{common_hash}"
)

def _open_ports(self) -> None:
"""Open ports if supported.

Used if `juju expose` ran on application
"""
if ops.JujuVersion.from_environ().supports_open_port_on_k8s:
try:
self.unit.set_ports(3306, 33060)
except ops.ModelError:
logger.exception("failed to open port")

def _write_mysqld_configuration(self) -> dict:
"""Write the mysqld configuration to the file."""
memory_limit_bytes = (self.config.profile_limit_memory or 0) * BYTES_1MB
Expand Down Expand Up @@ -725,7 +735,9 @@ def _configure_instance(self, container) -> None:

logger.info("Configuring initialized mysqld")
# Configure all base users and revoke privileges from the root users
self._mysql.configure_mysql_users()
self._mysql.configure_mysql_router_roles()
self._mysql.configure_mysql_system_roles()
self._mysql.configure_mysql_system_users()

if self.config.plugin_audit_enabled:
# Enable the audit plugin
Expand All @@ -737,6 +749,7 @@ def _configure_instance(self, container) -> None:
except (
MySQLInitialiseMySQLDError,
MySQLServiceNotRunningError,
MySQLConfigureMySQLRolesError,
MySQLConfigureMySQLUsersError,
MySQLConfigureInstanceError,
ChangeError,
Expand All @@ -758,8 +771,6 @@ def _configure_instance(self, container) -> None:
else:
container.start(MYSQLD_EXPORTER_SERVICE)

self._open_ports()

try:
# Set workload version
if workload_version := self._mysql.get_mysql_version():
Expand Down Expand Up @@ -840,23 +851,7 @@ def _on_mysql_pebble_ready(self, event) -> None:
self.join_unit_to_cluster()
return

try:
# Create the cluster when is the leader unit
logger.info(f"Creating cluster {self.app_peer_data['cluster-name']}")
self.unit.status = MaintenanceStatus("Creating cluster")
self.create_cluster()
self.unit.status = ops.ActiveStatus(self.active_status_message)

except (
MySQLCreateClusterError,
MySQLUnableToGetMemberStateError,
MySQLNoMemberStateError,
MySQLInitializeJujuOperationsTableError,
MySQLCreateClusterError,
):
logger.exception("Failed to initialize primary")
raise

self._create_cluster()
self._mysql.reconcile_binlogs_collection(force_restart=True)

def _handle_potential_cluster_crash_scenario(self) -> bool: # noqa: C901
Expand Down
6 changes: 4 additions & 2 deletions src/mysql_k8s_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ def _wait_until_unit_removed_from_cluster(self, unit_address: str) -> None:
if unit_address in members_in_cluster:
raise MySQLWaitUntilUnitRemovedFromClusterError("Remove member still in cluster")

def create_database(self, database_name: str) -> None:
def create_database_legacy(self, database_name: str) -> None:
"""Creates a database.

Args:
Expand All @@ -475,7 +475,9 @@ def create_database(self, database_name: str) -> None:
logger.exception(f"Failed to create database {database_name}", exc_info=e)
raise MySQLCreateDatabaseError(e.message)

def create_user(self, username: str, password: str, label: str, hostname: str = "%") -> None:
def create_user_legacy(
self, username: str, password: str, label: str, hostname: str = "%"
) -> None:
"""Creates a new user.

Args:
Expand Down
11 changes: 8 additions & 3 deletions src/relations/mysql.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@

from charms.mysql.v0.mysql import (
MySQLCheckUserExistenceError,
MySQLCreateApplicationDatabaseAndScopedUserError,
MySQLCreateApplicationDatabaseError,
MySQLCreateApplicationScopedUserError,
MySQLDeleteUsersForUnitError,
)
from ops.charm import RelationBrokenEvent, RelationChangedEvent, RelationCreatedEvent
Expand Down Expand Up @@ -245,14 +246,18 @@ def _on_mysql_relation_created(self, event: RelationCreatedEvent) -> None: # no

try:
logger.info("Creating application database and scoped user")
self.charm._mysql.create_application_database_and_scoped_user(
self.charm._mysql.create_database(database)
self.charm._mysql.create_scoped_user(
database,
username,
password,
"%",
unit_name="mysql-legacy-relation",
)
except MySQLCreateApplicationDatabaseAndScopedUserError:
except (
MySQLCreateApplicationDatabaseError,
MySQLCreateApplicationScopedUserError,
):
self.charm.unit.status = BlockedStatus(
"Failed to create application database and scoped user"
)
Expand Down
50 changes: 24 additions & 26 deletions src/relations/mysql_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@

from charms.data_platform_libs.v0.data_interfaces import DatabaseProvides, DatabaseRequestedEvent
from charms.mysql.v0.mysql import (
MySQLCreateApplicationDatabaseAndScopedUserError,
LEGACY_ROLE_ROUTER,
MODERN_ROLE_ROUTER,
MySQLCreateApplicationDatabaseError,
MySQLCreateApplicationScopedUserError,
MySQLDeleteUserError,
MySQLDeleteUsersForRelationError,
MySQLGetMySQLVersionError,
MySQLGrantPrivilegesToUserError,
MySQLRemoveRouterFromMetadataError,
)
from ops.charm import PebbleReadyEvent, RelationBrokenEvent, RelationDepartedEvent
Expand Down Expand Up @@ -107,16 +109,17 @@ def _on_database_requested(self, event: DatabaseRequestedEvent) -> None:

# get base relation data
relation_id = event.relation.id
app_name = event.app.name
db_name = event.database

extra_user_roles = []
if event.extra_user_roles:
extra_user_roles = event.extra_user_roles.split(",")

# user name is derived from the relation id
db_user = self._get_username(relation_id)
db_pass = self._get_or_set_password(event.relation)

remote_app = event.app.name

try:
# make sure pods are labeled before adding service
self.charm._mysql.update_endpoints(DB_RELATION_NAME)
Expand All @@ -132,25 +135,19 @@ def _on_database_requested(self, event: DatabaseRequestedEvent) -> None:
# wait for endpoints to be ready
self.charm.k8s_helpers.wait_service_ready((primary_endpoint, 3306))

if "mysqlrouter" in extra_user_roles:
self.charm._mysql.create_application_database_and_scoped_user(
db_name,
db_user,
db_pass,
"%",
# MySQL Router charm does not need a new database
create_database=False,
)
self.charm._mysql.grant_privileges_to_user(
db_user, "%", ["ALL PRIVILEGES"], with_grant_option=True
)
else:
# TODO:
# add setup of tls, tls_ca and status
# add extra roles parsing from relation data
self.charm._mysql.create_application_database_and_scoped_user(
db_name, db_user, db_pass, "%"
)
if not any([
LEGACY_ROLE_ROUTER in extra_user_roles,
MODERN_ROLE_ROUTER in extra_user_roles,
]):
self.charm._mysql.create_database(db_name)

self.charm._mysql.create_scoped_user(
db_name,
db_user,
db_pass,
"%",
extra_roles=extra_user_roles,
)

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

logger.info(f"Created user for app {remote_app}")
logger.info(f"Created user for app {app_name}")
self.charm.unit.status = ActiveStatus()
except (
MySQLCreateApplicationDatabaseAndScopedUserError,
MySQLCreateApplicationDatabaseError,
MySQLCreateApplicationScopedUserError,
MySQLGetMySQLVersionError,
MySQLGrantPrivilegesToUserError,
) as e:
logger.exception("Failed to set up database relation", exc_info=e)
self.charm.unit.status = BlockedStatus("Failed to create scoped user")
Expand Down
6 changes: 3 additions & 3 deletions src/relations/mysql_root.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,12 +201,12 @@ def _on_mysql_root_relation_created(self, event: RelationCreatedEvent) -> None:
try:
root_password = self.charm.get_secret("app", ROOT_PASSWORD_KEY)
assert root_password, "Root password not set"
self.charm._mysql.create_database(database)
self.charm._mysql.create_user(username, password, "mysql-root-legacy-relation")
self.charm._mysql.create_database_legacy(database)
self.charm._mysql.create_user_legacy(username, password, "mysql-root-legacy-relation")
if not self.charm._mysql.does_mysql_user_exist("root", "%"):
# create `root@%` user if it doesn't exist
# this is needed for the `mysql-root` interface to work
self.charm._mysql.create_user(
self.charm._mysql.create_user_legacy(
"root",
root_password,
"mysql-root-legacy-relation",
Expand Down
2 changes: 2 additions & 0 deletions tests/integration/roles/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright 2025 Canonical Ltd.
# See LICENSE file for licensing details.
Loading