Skip to content

[DPE-7322] Support predefined roles #652

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 2 commits 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
313 changes: 222 additions & 91 deletions lib/charms/mysql/v0/mysql.py

Large diffs are not rendered by default.

48 changes: 24 additions & 24 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
MySQLAddInstanceToClusterError,
MySQLCharmBase,
MySQLConfigureInstanceError,
MySQLConfigureMySQLRolesError,
MySQLConfigureMySQLUsersError,
MySQLCreateClusterError,
MySQLCreateClusterSetError,
Expand Down Expand Up @@ -326,6 +327,9 @@ def _on_start(self, event: StartEvent) -> None:

try:
self.workload_initialise()
except MySQLConfigureMySQLRolesError:
self.unit.status = BlockedStatus("Failed to initialize MySQL roles")
return
except MySQLConfigureMySQLUsersError:
self.unit.status = BlockedStatus("Failed to initialize MySQL users")
return
Expand All @@ -350,19 +354,7 @@ def _on_start(self, event: StartEvent) -> None:
self.unit_peer_data["member-state"] = "waiting"
return

try:
# Create the cluster and cluster set from the leader unit
logger.info(f"Creating cluster {self.app_peer_data['cluster-name']}")
self.create_cluster()
self._open_ports()
self.unit.status = ActiveStatus(self.active_status_message)
except (
MySQLCreateClusterError,
MySQLCreateClusterSetError,
MySQLInitializeJujuOperationsTableError,
) as e:
logger.exception("Failed to create cluster")
raise e
self._create_cluster()

def _on_peer_relation_changed(self, event: RelationChangedEvent) -> None:
"""Handle the peer relation changed event."""
Expand Down Expand Up @@ -770,7 +762,9 @@ def workload_initialise(self) -> None:
self._mysql.write_mysqld_config()
self.log_rotation_setup.setup()
self._mysql.reset_root_password_and_start_mysqld()
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:
self._mysql.install_plugins(["audit_log"])
Expand Down Expand Up @@ -806,16 +800,6 @@ def update_endpoints(self) -> None:
self.database_relation._update_endpoints_all_relations(None)
self._on_update_status(None)

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

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

def _can_start(self, event: StartEvent) -> bool:
"""Check if the unit can start.

Expand Down Expand Up @@ -865,6 +849,22 @@ def _can_start(self, event: StartEvent) -> bool:

return True

def _create_cluster(self) -> None:
"""Creates the InnoDB cluster and sets up the ports."""
try:
# Create the cluster and cluster set from the leader unit
logger.info(f"Creating cluster {self.app_peer_data['cluster-name']}")
self.create_cluster()
self.unit.set_ports(3306, 33060)
self.unit.status = ActiveStatus(self.active_status_message)
except (
MySQLCreateClusterError,
MySQLCreateClusterSetError,
MySQLInitializeJujuOperationsTableError,
) as e:
logger.exception("Failed to create cluster")
raise e

def _is_unit_waiting_to_join_cluster(self) -> bool:
"""Return if the unit is waiting to join the cluster."""
# alternatively, we could check if the instance is configured
Expand Down
3 changes: 2 additions & 1 deletion src/mysql_vm_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
MYSQLD_DEFAULTS_CONFIG_FILE,
MYSQLD_SOCK_FILE,
ROOT_SYSTEM_USER,
ROOT_USERNAME,
XTRABACKUP_PLUGIN_DIR,
)

Expand Down Expand Up @@ -819,7 +820,7 @@ def _run_mysqlsh_script(
def _run_mysqlcli_script(
self,
script: tuple[Any, ...] | list[Any],
user: str = "root",
user: str = ROOT_USERNAME,
password: str | None = None,
timeout: int | None = None,
exception_as_warning: bool = False,
Expand Down
13 changes: 8 additions & 5 deletions src/relations/db_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from charms.mysql.v0.mysql import (
MySQLCheckUserExistenceError,
MySQLConfigureRouterUserError,
MySQLCreateApplicationDatabaseAndScopedUserError,
MySQLCreateApplicationDatabaseError,
MySQLCreateApplicationScopedUserError,
MySQLDeleteUsersForUnitError,
MySQLGetClusterPrimaryAddressError,
)
Expand Down Expand Up @@ -124,8 +125,8 @@ def _create_requested_users(
Raises:
MySQLCheckUserExistenceError if there is an issue checking a user's existence
MySQLConfigureRouterUserError if there is an issue configuring the mysqlrouter user
MySQLCreateApplicationDatabaseAndScopedUserError if there is an issue creating a
user or said user scoped database
MySQLCreateApplicationDatabaseError if there is an issue creating the database
MySQLCreateApplicationScopedUserError if there is an issue creating the database user
"""
user_passwords = {}
requested_user_applications = set()
Expand All @@ -141,7 +142,8 @@ def _create_requested_users(
requested_user.username, password, requested_user.hostname, user_unit_name
)
else:
self.charm._mysql.create_application_database_and_scoped_user(
self.charm._mysql.create_database(requested_user.database)
self.charm._mysql.create_scoped_user(
requested_user.database,
requested_user.username,
password,
Expand Down Expand Up @@ -227,7 +229,8 @@ def _on_db_router_relation_changed(self, event: RelationChangedEvent) -> None:
except (
MySQLCheckUserExistenceError,
MySQLConfigureRouterUserError,
MySQLCreateApplicationDatabaseAndScopedUserError,
MySQLCreateApplicationDatabaseError,
MySQLCreateApplicationScopedUserError,
):
self.charm.unit.status = BlockedStatus("Failed to create app user or scoped database")
return
Expand Down
10 changes: 6 additions & 4 deletions src/relations/mysql.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@

from charms.mysql.v0.mysql import (
MySQLCheckUserExistenceError,
MySQLCreateApplicationDatabaseAndScopedUserError,
MySQLCreateApplicationDatabaseError,
MySQLCreateApplicationScopedUserError,
MySQLDeleteUsersForUnitError,
MySQLGetClusterPrimaryAddressError,
)
Expand Down Expand Up @@ -195,7 +196,8 @@ def _on_mysql_relation_created(self, event: RelationCreatedEvent) -> None:
password = self._get_or_set_password_in_peer_secrets(username)

try:
self.charm._mysql.create_application_database_and_scoped_user(
self.charm._mysql.create_database(database)
self.charm._mysql.create_scoped_user(
database,
username,
password,
Expand All @@ -204,9 +206,9 @@ def _on_mysql_relation_created(self, event: RelationCreatedEvent) -> None:
)

primary_address = self.charm._mysql.get_cluster_primary_address()

except (
MySQLCreateApplicationDatabaseAndScopedUserError,
MySQLCreateApplicationDatabaseError,
MySQLCreateApplicationScopedUserError,
MySQLGetClusterPrimaryAddressError,
):
self.charm.unit.status = BlockedStatus("Failed to initialize `mysql` relation")
Expand Down
52 changes: 25 additions & 27 deletions src/relations/mysql_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@

from charms.data_platform_libs.v0.data_interfaces import DatabaseProvides, DatabaseRequestedEvent
from charms.mysql.v0.mysql import (
LEGACY_ROLE_ROUTER,
MODERN_ROLE_ROUTER,
MySQLClientError,
MySQLCreateApplicationDatabaseAndScopedUserError,
MySQLCreateApplicationDatabaseError,
MySQLCreateApplicationScopedUserError,
MySQLDeleteUserError,
MySQLDeleteUsersForRelationError,
MySQLGetClusterEndpointsError,
MySQLGetClusterMembersAddressesError,
MySQLGetMySQLVersionError,
MySQLGrantPrivilegesToUserError,
MySQLRemoveRouterFromMetadataError,
)
from ops.charm import RelationBrokenEvent, RelationDepartedEvent, RelationJoinedEvent
from ops.framework import Object
from ops.model import BlockedStatus
from ops.model import ActiveStatus, BlockedStatus

from constants import DB_RELATION_NAME, PASSWORD_LENGTH, PEER
from utils import generate_random_password
Expand Down Expand Up @@ -220,16 +222,17 @@ def _on_database_requested(self, event: DatabaseRequestedEvent):

# 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

# Update endpoint addresses
self.charm.update_endpoint_address(DB_RELATION_NAME)

Expand All @@ -242,31 +245,26 @@ def _on_database_requested(self, event: DatabaseRequestedEvent):
self.database.set_version(relation_id, db_version)
self.database.set_read_only_endpoints(relation_id, ro_endpoints)

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
self.charm._mysql.create_application_database_and_scoped_user(
db_name, db_user, db_pass, "%"
)

logger.info(f"Created user for app {remote_app}")
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,
)
logger.info(f"Created user for app {app_name}")
self.charm.unit.status = ActiveStatus()
except (
MySQLCreateApplicationDatabaseAndScopedUserError,
MySQLCreateApplicationDatabaseError,
MySQLCreateApplicationScopedUserError,
MySQLGetMySQLVersionError,
MySQLGetClusterMembersAddressesError,
MySQLGrantPrivilegesToUserError,
MySQLClientError,
) as e:
logger.exception("Failed to set up database relation", exc_info=e)
Expand Down
17 changes: 13 additions & 4 deletions src/relations/shared_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
import typing

from charms.mysql.v0.mysql import (
MySQLCreateApplicationDatabaseAndScopedUserError,
MySQLCreateApplicationDatabaseError,
MySQLCreateApplicationScopedUserError,
MySQLGetClusterPrimaryAddressError,
)
from ops.charm import LeaderElectedEvent, RelationChangedEvent, RelationDepartedEvent
Expand Down Expand Up @@ -153,8 +154,13 @@ def _on_shared_db_relation_changed(self, event: RelationChangedEvent) -> None:
remote_host = event.relation.data[event.unit].get("private-address")

try:
self._charm._mysql.create_application_database_and_scoped_user(
database_name, database_user, password, remote_host, unit_name=joined_unit
self._charm._mysql.create_database(database_name)
self._charm._mysql.create_scoped_user(
database_name,
database_user,
password,
remote_host,
unit_name=joined_unit,
)

# set the relation data for consumption
Expand All @@ -179,7 +185,10 @@ def _on_shared_db_relation_changed(self, event: RelationChangedEvent) -> None:
allowed_units_set
)

except MySQLCreateApplicationDatabaseAndScopedUserError:
except (
MySQLCreateApplicationDatabaseError,
MySQLCreateApplicationScopedUserError,
):
self._charm.unit.status = BlockedStatus("Failed to initialize shared_db relation")
return

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