wordpress-charmers team mailing list archive
-
wordpress-charmers team
-
Mailing list archive
-
Message #00578
[Merge] ~stub/charm-k8s-wordpress:mysql-relation into charm-k8s-wordpress:master
Tom Haddon has proposed merging ~stub/charm-k8s-wordpress:mysql-relation into charm-k8s-wordpress:master.
Commit message:
Implement a MySQL database relation
The database connection details from the relation will
override any provided in the charm configuration.
Requested reviews:
Wordpress Charmers (wordpress-charmers)
Related bugs:
Bug #1854757 in charm-k8s-wordpress: "Add a mysql relation"
https://bugs.launchpad.net/charm-k8s-wordpress/+bug/1854757
For more details, see:
https://code.launchpad.net/~stub/charm-k8s-wordpress/+git/charm-k8s-wordpress/+merge/395826
MySQL endpoint implementation and charm support for a MySQL database relation. Seems to work with cs:~charmed-osm/mariadb-k8s. I suspect it won't work with cs:mysql, unless some kind soul has updated it to work with cross-model relations.
src/mysql.py can end up as github.com/canonical/ops-lib-mysql , where improvements can be made by someone who actually knows MySQL.
--
Your team Wordpress Charmers is requested to review the proposed merge of ~stub/charm-k8s-wordpress:mysql-relation into charm-k8s-wordpress:master.
diff --git a/metadata.yaml b/metadata.yaml
index eca73b7..89749b4 100644
--- a/metadata.yaml
+++ b/metadata.yaml
@@ -12,3 +12,7 @@ series:
provides:
website:
interface: http
+requires:
+ db:
+ interface: mysql
+ limit: 1
diff --git a/src/charm.py b/src/charm.py
index edade47..efe9026 100755
--- a/src/charm.py
+++ b/src/charm.py
@@ -6,13 +6,15 @@ import subprocess
from pprint import pprint
from yaml import safe_load
-from wordpress import Wordpress, password_generator, WORDPRESS_SECRETS
-
from ops.charm import CharmBase, CharmEvents
from ops.framework import EventBase, EventSource, StoredState
from ops.main import main
from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, WaitingStatus
+from mysql import MySQLClient
+from wordpress import Wordpress, password_generator, WORDPRESS_SECRETS
+
+
logger = logging.getLogger()
@@ -115,20 +117,20 @@ class WordpressCharm(CharmBase):
self.framework.observe(self.on.update_status, self.on_config_changed)
self.framework.observe(self.on.wordpress_initialise, self.on_wordpress_initialise)
+ self.db = MySQLClient(self, 'db')
+ self.framework.observe(self.on.db_relation_created, self.on_db_relation_created)
+ self.framework.observe(self.on.db_relation_broken, self.on_db_relation_broken)
+ self.framework.observe(self.db.on.database_changed, self.on_database_changed)
+
+ c = self.model.config
self.state.set_default(
- initialised=False, valid=False,
+ initialised=False, valid=False, has_db_relation=False,
+ db_host=c["db_host"], db_name=c["db_name"], db_user=c["db_user"], db_password=c["db_password"]
)
-
- self.wordpress = Wordpress(self.model.config)
+ self.wordpress = Wordpress(c)
def on_config_changed(self, event):
- is_valid = self.is_valid_config()
- if not is_valid:
- return
-
- self.configure_pod()
- if not self.state.initialised:
- self.on.wordpress_initialise.emit()
+ self.config_changed()
def on_wordpress_initialise(self, event):
wordpress_needs_configuring = False
@@ -161,6 +163,40 @@ class WordpressCharm(CharmBase):
logger.info("Wordpress workload pod is ready and configured")
self.model.unit.status = ActiveStatus()
+ def on_db_relation_created(self, event):
+ self.state.has_db_relation = True
+ self.state.db_host = None
+ self.state.db_name = None
+ self.state.db_user = None
+ self.state.db_password = None
+ self.config_changed()
+
+ def on_db_relation_broken(self, event):
+ self.state.has_db_relation = False
+ self.config_changed()
+
+ def on_database_changed(self, event):
+ self.state.db_host = event.host
+ self.state.db_name = event.database
+ self.state.db_user = event.user
+ self.state.db_password = event.password
+ self.config_changed()
+
+ def config_changed(self):
+ if not self.state.has_db_relation:
+ self.state.db_host = self.model.config["db_host"] or None
+ self.state.db_name = self.model.config["db_name"] or None
+ self.state.db_user = self.model.config["db_user"] or None
+ self.state.db_password = self.model.config["db_password"] or None
+
+ is_valid = self.is_valid_config()
+ if not is_valid:
+ return
+
+ self.configure_pod()
+ if not self.state.initialised:
+ self.on.wordpress_initialise.emit()
+
def configure_pod(self):
# Only the leader can set_spec().
if self.model.unit.is_leader():
@@ -220,7 +256,12 @@ class WordpressCharm(CharmBase):
return resources
def make_pod_spec(self):
- config = self.model.config
+ config = dict(self.model.config)
+ config["db_host"] = self.state.db_host
+ config["db_name"] = self.state.db_name
+ config["db_user"] = self.state.db_user
+ config["db_password"] = self.state.db_password
+
full_pod_config = generate_pod_config(config, secured=False)
full_pod_config.update(gather_wordpress_secrets())
secure_pod_config = generate_pod_config(config, secured=True)
@@ -264,10 +305,19 @@ class WordpressCharm(CharmBase):
self.model.unit.status = BlockedStatus("Missing initial_settings")
is_valid = False
- want = ("image", "db_host", "db_name", "db_user", "db_password", "tls_secret_name")
+ want = ["image", "tls_secret_name"]
+
+ if self.state.has_db_relation:
+ if not (self.state.db_host and self.state.db_name and self.state.db_user and self.state.db_password):
+ logger.info("MySQL relation has not yet provided database credentials.")
+ self.model.unit.status = WaitingStatus("Waiting for MySQL relation to become available")
+ is_valid = False
+ else:
+ want.extend(["db_host", "db_name", "db_user", "db_password"])
+
missing = [k for k in want if config[k].rstrip() == ""]
if missing:
- message = "Missing required config: {}".format(" ".join(missing))
+ message = "Missing required config or relation: {}".format(" ".join(missing))
logger.info(message)
self.model.unit.status = BlockedStatus(message)
is_valid = False
diff --git a/src/mysql.py b/src/mysql.py
new file mode 100644
index 0000000..2cbab87
--- /dev/null
+++ b/src/mysql.py
@@ -0,0 +1,166 @@
+"""
+MySQL endpoing 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)
Follow ups
-
[Merge] ~stub/charm-k8s-wordpress:mysql-relation into charm-k8s-wordpress:master
From: Stuart Bishop, 2021-01-12
-
[Merge] ~stub/charm-k8s-wordpress:mysql-relation into charm-k8s-wordpress:master
From: Tom Haddon, 2021-01-12
-
[Merge] ~stub/charm-k8s-wordpress:mysql-relation into charm-k8s-wordpress:master
From: Tom Haddon, 2021-01-12
-
[Merge] ~stub/charm-k8s-wordpress:mysql-relation into charm-k8s-wordpress:master
From: noreply, 2021-01-12
-
Re: [Merge] ~stub/charm-k8s-wordpress:mysql-relation into charm-k8s-wordpress:master
From: Canonical IS Mergebot, 2021-01-12
-
[Merge] ~stub/charm-k8s-wordpress:mysql-relation into charm-k8s-wordpress:master
From: Stuart Bishop, 2021-01-12
-
[Merge] ~stub/charm-k8s-wordpress:mysql-relation into charm-k8s-wordpress:master
From: Canonical IS Mergebot, 2021-01-12
-
Re: [Merge] ~stub/charm-k8s-wordpress:mysql-relation into charm-k8s-wordpress:master
From: Canonical IS Mergebot, 2021-01-12
-
[Merge] ~stub/charm-k8s-wordpress:mysql-relation into charm-k8s-wordpress:master
From: Tom Haddon, 2021-01-12
-
Re: [Merge] ~stub/charm-k8s-wordpress:mysql-relation into charm-k8s-wordpress:master
From: Tom Haddon, 2021-01-12
-
Re: [Merge] ~stub/charm-k8s-wordpress:mysql-relation into charm-k8s-wordpress:master
From: Tom Haddon, 2021-01-12
-
Re: [Merge] ~stub/charm-k8s-wordpress:mysql-relation into charm-k8s-wordpress:master
From: Canonical IS Mergebot, 2021-01-12