diff --git a/metadata.yaml b/metadata.yaml index ea1f386..6a7f1fe 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -16,3 +16,6 @@ provides: interface: http metrics-endpoint: interface: prometheus_scrape +peers: + dbcluster: + interface: dbcluster diff --git a/src/charm.py b/src/charm.py index 7c96368..f1a7547 100755 --- a/src/charm.py +++ b/src/charm.py @@ -6,8 +6,9 @@ import logging import secrets import yaml + from charms.prometheus_k8s.v0.prometheus_scrape import MetricsEndpointProvider -from ops.charm import CharmBase +from ops.charm import CharmBase, CollectStatusEvent from ops.framework import StoredState from ops.charm import RelationJoinedEvent, RelationDepartedEvent from ops.main import main @@ -21,13 +22,19 @@ class JujuControllerCharm(CharmBase): def __init__(self, *args): super().__init__(*args) + + self.framework.observe(self.on.collect_unit_status, self._on_collect_status) self.framework.observe(self.on.config_changed, self._on_config_changed) - self.framework.observe(self.on.start, self._on_start) self.framework.observe( self.on.dashboard_relation_joined, self._on_dashboard_relation_joined) self.framework.observe( self.on.website_relation_joined, self._on_website_relation_joined) + self._stored.set_default(db_bind_address='') + self._stored.set_default(last_bind_addresses=[]) + self.framework.observe( + self.on.dbcluster_relation_changed, self._on_dbcluster_relation_changed) + self.control_socket = controlsocket.Client( socket_path="/var/lib/juju/control.socket") self.framework.observe( @@ -35,11 +42,19 @@ def __init__(self, *args): self.framework.observe( self.on.metrics_endpoint_relation_broken, self._on_metrics_endpoint_relation_broken) - def _on_start(self, _): - self.unit.status = ActiveStatus() + def _on_collect_status(self, event: CollectStatusEvent): + if len(self._stored.last_bind_addresses) > 1: + event.add_status(BlockedStatus( + 'multiple possible DB bind addresses; set a suitable dbcluster network binding')) + + if self.api_port() is None: + event.add_status(BlockedStatus( + 'charm does not appear to be running on a controller node')) + + event.add_status(ActiveStatus()) def _on_config_changed(self, _): - controller_url = self.config["controller-url"] + controller_url = self.config['controller-url'] logger.info("got a new controller-url: %r", controller_url) def _on_dashboard_relation_joined(self, event): @@ -55,11 +70,10 @@ def _on_dashboard_relation_joined(self, event): def _on_website_relation_joined(self, event): """Connect a website relation.""" - logger.info("got a new website relation: %r", event) + logger.info('got a new website relation: %r', event) port = self.api_port() if port is None: - logger.error("machine does not appear to be a controller") - self.unit.status = BlockedStatus('machine does not appear to be a controller') + logger.error('charm does not appear to be running on a controller node') return address = None @@ -105,6 +119,22 @@ def _on_metrics_endpoint_relation_broken(self, event: RelationDepartedEvent): username = metrics_username(event.relation) self.control_socket.remove_metrics_user(username) + def _on_dbcluster_relation_changed(self, event): + ips = self.model.get_binding(event.relation).network.ingress_addresses + self._stored.last_bind_addresses = ips + + if len(ips) > 1: + logger.error( + 'multiple possible DB bind addresses; set a suitable bcluster network binding') + return + + ip = str(ips[0]) + if self._stored.db_bind_address == ip: + return + + self._stored.db_bind_address = ip + event.relation.data[self.unit].update({'db-bind-address': ip}) + def _agent_conf(self, key: str): """Read a value (by key) from the agent.conf file on disk.""" unit_name = self.unit.name.replace('/', '-') diff --git a/tests/test_charm.py b/tests/test_charm.py index 0585206..0a8580f 100644 --- a/tests/test_charm.py +++ b/tests/test_charm.py @@ -4,6 +4,7 @@ import os import unittest from charm import JujuControllerCharm +from ops.model import BlockedStatus, ActiveStatus from ops.testing import Harness from unittest.mock import mock_open, patch @@ -14,10 +15,14 @@ class TestCharm(unittest.TestCase): - def test_relation_joined(self): - harness = Harness(JujuControllerCharm) - self.addCleanup(harness.cleanup) - harness.begin() + def setUp(self): + self.harness = Harness(JujuControllerCharm) + self.addCleanup(self.harness.cleanup) + self.harness.begin() + + def test_dashboard_relation_joined(self): + harness = self.harness + harness.set_leader(True) harness.update_config({"controller-url": "wss://controller/api"}) harness.update_config({"identity-provider-url": ""}) @@ -36,12 +41,10 @@ def test_relation_joined(self): }) @patch("ops.model.Model.get_binding") @patch("builtins.open", new_callable=mock_open, read_data=agent_conf) - def test_website_relation_joined(self, _, ingress_address): - ingress_address.return_value = MockBinding("192.168.1.17") + def test_website_relation_joined(self, _, binding): + harness = self.harness + binding.return_value = mockBinding(["192.168.1.17"]) - harness = Harness(JujuControllerCharm) - self.addCleanup(harness.cleanup) - harness.begin() harness.set_leader() relation_id = harness.add_relation('website', 'haproxy') harness.add_relation_unit(relation_id, 'haproxy/0') @@ -58,10 +61,7 @@ def test_website_relation_joined(self, _, ingress_address): @patch("controlsocket.Client.remove_metrics_user") def test_metrics_endpoint_relation(self, mock_remove_user, mock_add_user, mock_metrics_provider, _): - harness = Harness(JujuControllerCharm) - self.addCleanup(harness.cleanup) - harness.begin() - + harness = self.harness harness.add_network(address="192.168.1.17", endpoint="metrics-endpoint") relation_id = harness.add_relation('metrics-endpoint', 'prometheus-k8s') @@ -88,12 +88,46 @@ def test_metrics_endpoint_relation(self, mock_remove_user, mock_add_user, harness.remove_relation(relation_id) mock_remove_user.assert_called_once_with(f'juju-metrics-r{relation_id}') + @patch("builtins.open", new_callable=mock_open, read_data=agent_conf) + @patch("ops.model.Model.get_binding") + def test_dbcluster_relation_changed_single_addr(self, binding, _): + harness = self.harness + binding.return_value = mockBinding(["192.168.1.17"]) + + relation_id = harness.add_relation('dbcluster', 'controller') + harness.add_relation_unit(relation_id, 'juju-controller/1') + + harness.charm._on_dbcluster_relation_changed( + harness.charm.model.get_relation('dbcluster').data[harness.charm.unit]) + + data = harness.get_relation_data(relation_id, 'juju-controller/0') + self.assertEqual(data["db-bind-address"], "192.168.1.17") + + harness.evaluate_status() + self.assertIsInstance(harness.charm.unit.status, ActiveStatus) + + @patch("builtins.open", new_callable=mock_open, read_data=agent_conf) + @patch("ops.model.Model.get_binding") + def test_dbcluster_relation_changed_multi_addr_error(self, binding, _): + harness = self.harness + binding.return_value = mockBinding(["192.168.1.17", "192.168.1.18"]) + + relation_id = harness.add_relation('dbcluster', 'controller') + harness.add_relation_unit(relation_id, 'juju-controller/1') + + harness.charm._on_dbcluster_relation_changed( + harness.charm.model.get_relation('dbcluster').data[harness.charm.unit]) + + harness.evaluate_status() + self.assertIsInstance(harness.charm.unit.status, BlockedStatus) + -class MockBinding: - def __init__(self, address): - self.network = MockNetwork(address) +class mockNetwork: + def __init__(self, addresses): + self.ingress_addresses = addresses + self.ingress_address = addresses[0] -class MockNetwork: - def __init__(self, address): - self.ingress_address = address +class mockBinding: + def __init__(self, addresses): + self.network = mockNetwork(addresses)