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 7c96368..302014b 100755 --- a/src/charm.py +++ b/src/charm.py @@ -5,13 +5,15 @@ import controlsocket import logging import secrets +import urllib.parse import yaml from charms.prometheus_k8s.v0.prometheus_scrape import MetricsEndpointProvider from ops.charm import CharmBase 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__) @@ -79,6 +81,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=[{ @@ -86,7 +94,7 @@ def _on_metrics_endpoint_relation_created(self, event: RelationJoinedEvent): "scheme": "https", "static_configs": [{ "targets": [ - f'*:{self.api_port()}' + f'*:{api_port}' ] }], "basic_auth": { @@ -116,7 +124,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.""" @@ -136,5 +153,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 0585206..3d51315 100644 --- a/tests/test_charm.py +++ b/tests/test_charm.py @@ -3,12 +3,36 @@ import os import unittest -from charm import JujuControllerCharm +from charm import JujuControllerCharm, AgentConfException +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,6 +112,54 @@ 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) + class MockBinding: def __init__(self, address):