diff --git a/run_tests b/run_tests index e47ee64..9c0bf7a 100755 --- a/run_tests +++ b/run_tests @@ -11,6 +11,6 @@ if [ -n "$PYTHONPATH" ]; then fi export PYTHONPATH="src:lib$PYTHONPATH" -flake8 coverage run --source=src -m unittest -v "$@" coverage report -m +flake8 diff --git a/src/charm.py b/src/charm.py index f1a7547..828d5ac 100755 --- a/src/charm.py +++ b/src/charm.py @@ -5,6 +5,7 @@ import controlsocket import logging import secrets +import urllib.parse import yaml from charms.prometheus_k8s.v0.prometheus_scrape import MetricsEndpointProvider @@ -12,7 +13,8 @@ from ops.framework import StoredState from ops.charm import RelationJoinedEvent, RelationDepartedEvent from ops.main import main -from ops.model import ActiveStatus, BlockedStatus, Relation +from ops.model import ActiveStatus, BlockedStatus, ErrorStatus, Relation +from typing import List logger = logging.getLogger(__name__) @@ -93,6 +95,12 @@ def _on_metrics_endpoint_relation_created(self, event: RelationJoinedEvent): self.control_socket.add_metrics_user(username, password) # Set up Prometheus scrape config + try: + api_port = self.api_port() + except AgentConfException as e: + self.unit.status = ErrorStatus(f"can't read controller API port from agent.conf: {e}") + return + metrics_endpoint = MetricsEndpointProvider( self, jobs=[{ @@ -100,7 +108,7 @@ def _on_metrics_endpoint_relation_created(self, event: RelationJoinedEvent): "scheme": "https", "static_configs": [{ "targets": [ - f'*:{self.api_port()}' + f'*:{api_port}' ] }], "basic_auth": { @@ -146,7 +154,16 @@ def _agent_conf(self, key: str): def api_port(self) -> str: """Return the port on which the controller API server is listening.""" - return self._agent_conf('apiport') + api_addresses = self._agent_conf('apiaddresses') + if not api_addresses: + raise AgentConfException("agent.conf key 'apiaddresses' missing") + if not isinstance(api_addresses, List): + raise AgentConfException("agent.conf key 'apiaddresses' is not a list") + + parsed_url = urllib.parse.urlsplit('//' + api_addresses[0]) + if not parsed_url.port: + raise AgentConfException("api address doesn't include port") + return parsed_url.port def ca_cert(self) -> str: """Return the controller's CA certificate.""" @@ -166,5 +183,9 @@ def generate_password() -> str: return secrets.token_urlsafe(16) +class AgentConfException(Exception): + """Raised when there are errors reading info from agent.conf.""" + + if __name__ == "__main__": main(JujuControllerCharm) diff --git a/tests/test_charm.py b/tests/test_charm.py index 0a8580f..da81801 100644 --- a/tests/test_charm.py +++ b/tests/test_charm.py @@ -3,13 +3,37 @@ import os import unittest -from charm import JujuControllerCharm +from charm import JujuControllerCharm, AgentConfException from ops.model import BlockedStatus, ActiveStatus +from ops import ErrorStatus from ops.testing import Harness from unittest.mock import mock_open, patch agent_conf = ''' -apiport: 17070 +apiaddresses: +- localhost:17070 +cacert: fake +''' + +agent_conf_apiaddresses_missing = ''' +cacert: fake +''' + +agent_conf_apiaddresses_not_list = ''' +apiaddresses: + foo: bar +cacert: fake +''' + +agent_conf_ipv4 = ''' +apiaddresses: +- "127.0.0.1:17070" +cacert: fake +''' + +agent_conf_ipv6 = ''' +apiaddresses: +- "[::1]:17070" cacert: fake ''' @@ -88,12 +112,59 @@ 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_apiaddresses_missing) + def test_apiaddresses_missing(self, _): + harness = Harness(JujuControllerCharm) + self.addCleanup(harness.cleanup) + harness.begin() + + with self.assertRaisesRegex(AgentConfException, "agent.conf key 'apiaddresses' missing"): + harness.charm.api_port() + + @patch("builtins.open", new_callable=mock_open, read_data=agent_conf_apiaddresses_not_list) + def test_apiaddresses_not_list(self, _): + harness = Harness(JujuControllerCharm) + self.addCleanup(harness.cleanup) + harness.begin() + + with self.assertRaisesRegex( + AgentConfException, "agent.conf key 'apiaddresses' is not a list" + ): + harness.charm.api_port() + + @patch("builtins.open", new_callable=mock_open, read_data=agent_conf_apiaddresses_missing) + @patch("controlsocket.Client.add_metrics_user") + def test_apiaddresses_missing_status(self, *_): + harness = Harness(JujuControllerCharm) + self.addCleanup(harness.cleanup) + harness.begin() + + harness.add_relation('metrics-endpoint', 'prometheus-k8s') + self.assertEqual(harness.charm.unit.status, ErrorStatus( + "can't read controller API port from agent.conf: agent.conf key 'apiaddresses' missing" + )) + + @patch("builtins.open", new_callable=mock_open, read_data=agent_conf_ipv4) + def test_apiaddresses_ipv4(self, _): + harness = Harness(JujuControllerCharm) + self.addCleanup(harness.cleanup) + harness.begin() + + self.assertEqual(harness.charm.api_port(), 17070) + + @patch("builtins.open", new_callable=mock_open, read_data=agent_conf_ipv6) + def test_apiaddresses_ipv6(self, _): + harness = Harness(JujuControllerCharm) + self.addCleanup(harness.cleanup) + harness.begin() + + self.assertEqual(harness.charm.api_port(), 17070) + @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')