← Back to team overview

wordpress-charmers team mailing list archive

[Merge] ~stub/charm-k8s-wordpress:ops-lib-mysql into charm-k8s-wordpress:master

 

Stuart Bishop has proposed merging ~stub/charm-k8s-wordpress:ops-lib-mysql into charm-k8s-wordpress:master.

Commit message:
Switch to ops-lib-mysql

Move the embedded mysql endpoint implementation to an external
library for sharing with other charms.

Requested reviews:
  Wordpress Charmers (wordpress-charmers)

For more details, see:
https://code.launchpad.net/~stub/charm-k8s-wordpress/+git/charm-k8s-wordpress/+merge/396227
-- 
Your team Wordpress Charmers is requested to review the proposed merge of ~stub/charm-k8s-wordpress:ops-lib-mysql into charm-k8s-wordpress:master.
diff --git a/requirements.txt b/requirements.txt
index 875fb9a..52d0cf0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
 # Include python requirements here
 ops
 requests
+ops-lib-mysql
diff --git a/src/charm.py b/src/charm.py
index f1a2252..990d55d 100755
--- a/src/charm.py
+++ b/src/charm.py
@@ -12,7 +12,7 @@ from ops.main import main
 from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, WaitingStatus
 from leadership import LeadershipSettings
 
-from mysql import MySQLClient
+from opslib.mysql import MySQLClient
 from wordpress import Wordpress, password_generator, WORDPRESS_SECRETS
 
 
diff --git a/src/mysql.py b/src/mysql.py
deleted file mode 100644
index 8429b38..0000000
--- a/src/mysql.py
+++ /dev/null
@@ -1,166 +0,0 @@
-"""
-MySQL endpoint implementation for the Operator Framework.
-
-Ported to the Operator Framework from the canonical-osm Reactive
-charms at https://git.launchpad.net/canonical-osm
-"""
-
-import logging
-
-import ops.charm
-import ops.framework
-import ops.model
-
-
-__all__ = ["MySQLClient", "MySQLClientEvents", "MySQLRelationEvent", "MySQLDatabaseChangedEvent"]
-
-
-class _MySQLConnectionDetails(object):
-    database: str = None
-    host: str = None
-    port: int = 3306
-    user: str = None
-    password: str = None
-    root_password: str = None
-    connection_string: str = None
-    sanitized_connection_string: str = None  # With no secrets, for logging.
-    is_available: bool = False
-
-    def __init__(self, relation: ops.model.Relation, unit: ops.model.Unit):
-        reldata = relation.data.get(unit, {})
-        self.database = reldata.get("database", None)
-        self.host = reldata.get("host", None)
-        self.port = int(reldata.get("port", 3306))
-        self.user = reldata.get("user", None)
-        self.password = reldata.get("password", None)
-        self.root_password = reldata.get("root_password", None)
-
-        if all([self.database, self.host, self.port, self.user, self.password, self.root_password]):
-            self.sanitized_connection_string = (
-                f"host={self.host} port={self.port} dbname={self.database} user={self.user}"
-            )
-            self.connection_string = (
-                self.sanitized_connection_string + f" password={self.password} root_password={self.root_password}"
-            )
-        else:
-            self.sanitized_connection_string = None
-            self.connection_string = None
-        self.is_available = self.connection_string is not None
-
-
-class MySQLRelationEvent(ops.charm.RelationEvent):
-    def __init__(self, *args, **kw):
-        super().__init__(*args, **kw)
-        self._conn = _MySQLConnectionDetails(self.relation, self.unit)
-
-    @property
-    def is_available(self) -> bool:
-        """True if the database is available for use."""
-        return self._conn.is_available
-
-    @property
-    def connection_string(self) -> str:
-        """The connection string, if available, or None.
-
-        The connection string will be in the format:
-
-            'host={host} port={port} dbname={database} user={user} password={password} root_password={root_password}'
-        """
-        return self._conn.connection_string
-
-    @property
-    def database(self) -> str:
-        """The name of the provided database, or None."""
-        return self._conn.database
-
-    @property
-    def host(self) -> str:
-        """The host for the provided database, or None."""
-        return self._conn.host
-
-    @property
-    def port(self) -> int:
-        """The port to the provided database."""
-        # If not available, returns the default port of 3306.
-        return self._conn.port
-
-    @property
-    def user(self) -> str:
-        """The username for the provided database, or None."""
-        return self._conn.user
-
-    @property
-    def password(self) -> str:
-        """The password for the provided database, or None."""
-        return self._conn.password
-
-    @property
-    def root_password(self) -> str:
-        """The password for the root user, or None."""
-        return self._conn.root_password
-
-    def restore(self, snapshot) -> None:
-        super().restore(snapshot)
-        self._conn = _MySQLConnectionDetails(self.relation, self.unit)
-
-
-class MySQLDatabaseChangedEvent(MySQLRelationEvent):
-    """The database connection details on the relation have changed.
-
-    This event is emitted when the database first becomes available
-    for use, when the connection details have changed, and when it
-    becomes unavailable.
-    """
-
-    pass
-
-
-class MySQLClientEvents(ops.framework.ObjectEvents):
-    database_changed = ops.framework.EventSource(MySQLDatabaseChangedEvent)
-
-
-class MySQLClient(ops.framework.Object):
-    """Requires side of a MySQL Endpoint"""
-
-    on = MySQLClientEvents()
-    _state = ops.framework.StoredState()
-
-    relation_name: str = None
-    log: logging.Logger = None
-
-    def __init__(self, charm: ops.charm.CharmBase, relation_name: str):
-        super().__init__(charm, relation_name)
-
-        self.relation_name = relation_name
-        self.log = logging.getLogger("mysql.client.{}".format(relation_name))
-        self._state.set_default(rels={})
-
-        self.framework.observe(charm.on[relation_name].relation_changed, self._on_changed)
-        self.framework.observe(charm.on[relation_name].relation_broken, self._on_broken)
-
-    def _on_changed(self, event: ops.charm.RelationEvent) -> None:
-        if event.unit is None:
-            return  # Ignore application relation data events.
-
-        prev_conn_str = self._state.rels.get(event.relation.id, None)
-        new_cd = _MySQLConnectionDetails(event.relation, event.unit)
-        new_conn_str = new_cd.connection_string
-
-        if prev_conn_str != new_conn_str:
-            self._state.rels[event.relation.id] = new_conn_str
-            if new_conn_str is None:
-                self.log.info(f"Database on relation {event.relation.id} is no longer available.")
-            else:
-                self.log.info(
-                    f"Database on relation {event.relation.id} available at {new_cd.sanitized_connection_string}."
-                )
-            self.on.database_changed.emit(relation=event.relation, app=event.app, unit=event.unit)
-
-    def _on_broken(self, event: ops.charm.RelationEvent) -> None:
-        self.log.info(f"Database relation {event.relation.id} is gone.")
-        prev_conn_str = self._state.rels.get(event.relation.id, None)
-        if event.relation.id in self._state.rels:
-            del self._state.rels[event.relation.id]
-        if prev_conn_str is None:
-            return
-        self.on.database_changed.emit(relation=event.relation, app=event.app, unit=None)
diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py
index acf406c..bd4d237 100644
--- a/tests/unit/test_charm.py
+++ b/tests/unit/test_charm.py
@@ -30,10 +30,40 @@ class TestWordpressCharm(unittest.TestCase):
 
     def setUp(self):
         self.harness = testing.Harness(WordpressCharm)
+        self.addCleanup(self.harness.cleanup)
 
         self.harness.begin()
         self.harness.update_config(copy.deepcopy(self.test_model_config))
 
+    def test_db_relation(self):
+        # Charm starts with no relation, defaulting to using db
+        # connection details from the charm config.
+        charm = self.harness.charm
+        self.assertFalse(charm.state.has_db_relation)
+        self.assertEqual(charm.state.db_host, TEST_MODEL_CONFIG["db_host"])
+        self.assertEqual(charm.state.db_name, TEST_MODEL_CONFIG["db_name"])
+        self.assertEqual(charm.state.db_user, TEST_MODEL_CONFIG["db_user"])
+        self.assertEqual(charm.state.db_password, TEST_MODEL_CONFIG["db_password"])
+
+        # Add a relation and remote unit providing connection details.
+        # TODO: ops-lib-mysql should have a helper to set the relation data.
+        relid = self.harness.add_relation("db", "mysql")
+        self.harness.add_relation_unit(relid, "mysql/0")
+        self.harness.update_relation_data(relid, "mysql/0", {
+            "database": "wpdbname",
+            "host": "hostname.local",
+            "port": "3306",
+            "user": "wpuser",
+            "password": "s3cret",
+            "root_password": "sup3r_s3cret",
+        })
+        # charm.db.on.database_changed fires here and is handled, updating state.
+        self.assertTrue(charm.state.has_db_relation)
+        self.assertEqual(charm.state.db_host, "hostname.local")
+        self.assertEqual(charm.state.db_name, "wpdbname")
+        self.assertEqual(charm.state.db_user, "wpuser")
+        self.assertEqual(charm.state.db_password, "s3cret")
+
     def test_is_config_valid(self):
         # Test a valid model config.
         want_true = self.harness.charm.is_valid_config()

References