sunbeam-charms/ops-sunbeam/doc/howto-relation-handler.rst
Guillaume Boutry 1cf33168ff
Reference ops.Object from its definition package
ops's Object is defined in the framework package. The charm package is
only a user.

Change-Id: I3683f30fed9cda28b25efce21c40beaf447e1fca
2023-12-06 13:45:10 +01:00

140 lines
5.2 KiB
ReStructuredText

===============================
How-To Write a relation handler
===============================
A relation handler gives the charm a consistent method of interacting with
relation interfaces. It can also encapsulate common interface tasks, this
removes the need for duplicate code across multiple charms.
This how-to will walk through the steps to write a database relation handler
for the requires side.
In this database interface the database charm expects the client to provide the name
of the database(s) to be created. To model this the relation handler will require
the charm to specify the database name(s) when the class is instantiated.
.. code:: python
class DBHandler(RelationHandler):
"""Handler for DB relations."""
def __init__(
self,
charm: ops.charm.CharmBase,
relation_name: str,
callback_f: Callable,
databases: List[str] = None,
) -> None:
"""Run constructor."""
self.databases = databases
super().__init__(charm, relation_name, callback_f)
The handler initialises the interface with the database names and also sets up
an observer for relation changed events.
.. code:: python
def setup_event_handler(self) -> ops.framework.Object:
"""Configure event handlers for a MySQL relation."""
logger.debug("Setting up DB event handler")
# Lazy import to ensure this lib is only required if the charm
# has this relation.
import charms.sunbeam_mysql_k8s.v0.mysql as mysql
db = mysql.MySQLConsumer(
self.charm, self.relation_name, databases=self.databases
)
_rname = self.relation_name.replace("-", "_")
db_relation_event = getattr(
self.charm.on, f"{_rname}_relation_changed"
)
self.framework.observe(db_relation_event, self._on_database_changed)
return db
The method runs when the changed event is seen and checks whether all required
data has been provided. If it is then it calls back to the charm, if not then
no action is taken.
.. code:: python
def _on_database_changed(self, event: ops.framework.EventBase) -> None:
"""Handle database change events."""
databases = self.interface.databases()
logger.info(f"Received databases: {databases}")
if not self.ready:
return
self.callback_f(event)
@property
def ready(self) -> bool:
"""Whether the handler is ready for use."""
try:
# Nothing to wait for
return bool(self.interface.databases())
except (AttributeError, KeyError):
return False
The `ready` property is common across all handlers and allows the charm to
check the state of any relation in a consistent way.
The relation handlers also provide a context which can be used when rendering
templates. ASO places each relation context in its own namespace.
.. code:: python
def context(self) -> dict:
"""Context containing database connection data."""
try:
databases = self.interface.databases()
except (AttributeError, KeyError):
return {}
if not databases:
return {}
ctxt = {}
conn_data = {
"database_host": self.interface.credentials().get("address"),
"database_password": self.interface.credentials().get("password"),
"database_user": self.interface.credentials().get("username"),
"database_type": "mysql+pymysql",
}
for db in self.interface.databases():
ctxt[db] = {"database": db}
ctxt[db].update(conn_data)
connection = (
"{database_type}://{database_user}:{database_password}"
"@{database_host}/{database}")
if conn_data.get("database_ssl_ca"):
connection = connection + "?ssl_ca={database_ssl_ca}"
if conn_data.get("database_ssl_cert"):
connection = connection + (
"&ssl_cert={database_ssl_cert}"
"&ssl_key={database_ssl_key}")
ctxt[db]["connection"] = str(connection.format(
**ctxt[db]))
return ctxt
Configuring Charm to use custom relation handler
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The base class will add the default relation handlers for any interfaces
which do not yet have a handler. Therefore the custom handler is added to
the list and then passed to the super method. The base charm class will
see a handler already exists for database and not add the default one.
.. code:: python
class MyCharm(sunbeam_charm.OSBaseOperatorAPICharm):
"""Charm the service."""
def get_relation_handlers(self, handlers=None) -> List[
sunbeam_rhandlers.RelationHandler]:
"""Relation handlers for the service."""
handlers = handlers or []
if self.can_add_handler("database", handlers):
self.db = sunbeam_rhandlers.DBHandler(
self, "database", self.configure_charm, self.databases
)
handlers.append(self.db)
handlers = super().get_relation_handlers(handlers)
return handlers