From f8c9cd0f770bf7a53391a22d8f7dc5f184dee33a Mon Sep 17 00:00:00 2001 From: Stamatis Katsaounis Date: Wed, 28 Aug 2024 14:24:24 +0300 Subject: [PATCH] chore: release 3.4/edge with TLS termination changes, deps updates (#210) * feat: publish charms to track edges based on branch name (#190) (cherry picked from commit f569e0f131742acac3569e60d88b58aacf7f80e4) * chore: update charm libraries (#193) (cherry picked from commit 13668bf66ac31763e2c5eace904163589ac4eaaf) * feat: add configuration entry for TLS termination at HA Proxy (#194) adds a tls_mode configuration option, which determines how HA Proxy's services yaml configuration should be updated (cherry picked from commit ac0ba6960dc3736eda687d7b679a94ab32895f74) --------- Co-authored-by: maas-lander <115650013+maas-lander@users.noreply.github.com> Co-authored-by: Wyatt Rees --- .github/workflows/release.yaml | 1 + .../lib/charms/grafana_agent/v0/cos_agent.py | 8 ++- maas-region/charmcraft.yaml | 7 +++ maas-region/src/charm.py | 35 +++++++++++- maas-region/tests/integration/test_charm.py | 55 ++++++++++++++++++- maas-region/tests/unit/test_charm.py | 29 ++++++++++ 6 files changed, 131 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 63e889a..cc770c0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -4,6 +4,7 @@ on: push: branches: - main + - track/* jobs: detect-region-changes: diff --git a/maas-agent/lib/charms/grafana_agent/v0/cos_agent.py b/maas-agent/lib/charms/grafana_agent/v0/cos_agent.py index 582b70c..cc4da25 100644 --- a/maas-agent/lib/charms/grafana_agent/v0/cos_agent.py +++ b/maas-agent/lib/charms/grafana_agent/v0/cos_agent.py @@ -22,7 +22,7 @@ Using the `COSAgentProvider` object only requires instantiating it, typically in the `__init__` method of your charm (the one which sends telemetry). -The constructor of `COSAgentProvider` has only one required and nine optional parameters: +The constructor of `COSAgentProvider` has only one required and ten optional parameters: ```python def __init__( @@ -36,6 +36,7 @@ def __init__( log_slots: Optional[List[str]] = None, dashboard_dirs: Optional[List[str]] = None, refresh_events: Optional[List] = None, + tracing_protocols: Optional[List[str]] = None, scrape_configs: Optional[Union[List[Dict], Callable]] = None, ): ``` @@ -65,6 +66,8 @@ def __init__( - `refresh_events`: List of events on which to refresh relation data. +- `tracing_protocols`: List of requested tracing protocols that the charm requires to send traces. + - `scrape_configs`: List of standard scrape_configs dicts or a callable that returns the list in case the configs need to be generated dynamically. The contents of this list will be merged with the configs from `metrics_endpoints`. @@ -108,6 +111,7 @@ def __init__(self, *args): log_slots=["my-app:slot"], dashboard_dirs=["./src/dashboards_1", "./src/dashboards_2"], refresh_events=["update-status", "upgrade-charm"], + tracing_protocols=["otlp_http", "otlp_grpc"], scrape_configs=[ { "job_name": "custom_job", @@ -249,7 +253,7 @@ class _MetricsEndpointDict(TypedDict): LIBID = "dc15fa84cef84ce58155fb84f6c6213a" LIBAPI = 0 -LIBPATCH = 10 +LIBPATCH = 11 PYDEPS = ["cosl", "pydantic"] diff --git a/maas-region/charmcraft.yaml b/maas-region/charmcraft.yaml index ca1beb8..81bfa19 100644 --- a/maas-region/charmcraft.yaml +++ b/maas-region/charmcraft.yaml @@ -126,3 +126,10 @@ parts: "*": src/loki/ prime: - src/loki/loki.yml + +config: + options: + tls_mode: + default: "" + description: Whether to enable TLS termination at HA Proxy ('termination'), or no TLS ('') + type: string diff --git a/maas-region/src/charm.py b/maas-region/src/charm.py index c9be985..88a08a1 100755 --- a/maas-region/src/charm.py +++ b/maas-region/src/charm.py @@ -56,6 +56,11 @@ class MaasRegionCharm(ops.CharmBase): """Charm the application.""" + _TLS_MODES = [ + "", + "termination", + ] # no TLS, termination at HA Proxy + def __init__(self, *args): super().__init__(*args) @@ -109,6 +114,9 @@ def __init__(self, *args): self.framework.observe(self.on.list_controllers_action, self._on_list_controllers_action) self.framework.observe(self.on.get_api_endpoint_action, self._on_get_api_endpoint_action) + # Charm configuration + self.framework.observe(self.on.config_changed, self._on_config_changed) + @property def peers(self) -> Union[ops.Relation, None]: """Fetch the peer relation.""" @@ -265,8 +273,25 @@ def _update_ha_proxy(self) -> None: [], ) ], - } + }, ] + if self.config["tls_mode"] == "termination": + data.append( + { + "service_name": "agent_service", + "service_host": "0.0.0.0", + "service_port": MAAS_PROXY_PORT, + "servers": [ + ( + f"{app_name}-{self.unit.name.replace('/', '-')}", + self.bind_address, + MAAS_HTTP_PORT, + [], + ) + ], + } + ) + # TODO: Implement passthrough configuration relation.data[self.unit]["services"] = yaml.safe_dump(data) def _on_start(self, _event: ops.StartEvent) -> None: @@ -408,6 +433,14 @@ def _on_get_api_endpoint_action(self, event: ops.ActionEvent): else: event.fail("MAAS is not initialized yet") + def _on_config_changed(self, event: ops.ConfigChangedEvent): + tls_mode = self.config["tls_mode"] + if tls_mode not in self._TLS_MODES: + msg = f"Invalid tls_mode configuration: '{tls_mode}'. Valid options are: {self._TLS_MODES}" + self.unit.status = ops.BlockedStatus(msg) + raise ValueError(msg) + self._update_ha_proxy() + if __name__ == "__main__": # pragma: nocover ops.main(MaasRegionCharm) # type: ignore diff --git a/maas-region/tests/integration/test_charm.py b/maas-region/tests/integration/test_charm.py index 6dae890..a3e7b8e 100644 --- a/maas-region/tests/integration/test_charm.py +++ b/maas-region/tests/integration/test_charm.py @@ -4,7 +4,9 @@ import asyncio import logging +import time from pathlib import Path +from subprocess import check_output import pytest import yaml @@ -27,7 +29,9 @@ async def test_build_and_deploy(ops_test: OpsTest): # Deploy the charm and wait for waiting/idle status await asyncio.gather( - ops_test.model.deploy(charm, application_name=APP_NAME), + ops_test.model.deploy( + charm, application_name=APP_NAME, config={"tls_mode": "termination"} + ), ops_test.model.wait_for_idle( apps=[APP_NAME], status="waiting", raise_on_blocked=True, timeout=1000 ), @@ -58,3 +62,52 @@ async def test_database_integration(ops_test: OpsTest): apps=[APP_NAME], status="active", raise_on_blocked=True, timeout=1000 ), ) + + +@pytest.mark.abort_on_fail +async def test_tls_mode(ops_test: OpsTest): + """Verify that the charm tls_mode configuration option works as expected. + + Assert that the agent_service is properly set up. + """ + # Deploy the charm and haproxy and wait for active/waiting status + await asyncio.gather( + ops_test.model.deploy( + "haproxy", + application_name="haproxy", + channel="latest/stable", + trust=True, + ), + ops_test.model.wait_for_idle( + apps=["haproxy"], status="active", raise_on_blocked=True, timeout=1000 + ), + ) + await ops_test.model.integrate(f"{APP_NAME}", "haproxy") + # the relation may take some time beyond the above await to fully apply + start = time.time() + timeout = 30 + while True: + try: + show_unit = check_output( + f"JUJU_MODEL={ops_test.model.name} juju show-unit haproxy/0", + shell=True, + universal_newlines=True, + ) + result = yaml.safe_load(show_unit) + services_str = result["haproxy/0"]["relation-info"][1]["related-units"][ + "maas-region/0" + ]["data"]["services"] + break + except KeyError: + time.sleep(1) + if time.time() > start + timeout: + pytest.fail("Timed out waiting for relation data to apply") + + services_yaml = yaml.safe_load(services_str) + + assert len(services_yaml) == 2 + assert services_yaml[1]["service_name"] == "agent_service" + assert services_yaml[1]["service_port"] == 80 + agent_server = services_yaml[1]["servers"][0] + assert agent_server[0] == "api-maas-region-maas-region-0" + assert agent_server[2] == 5240 diff --git a/maas-region/tests/unit/test_charm.py b/maas-region/tests/unit/test_charm.py index 52add64..2c3bc2b 100644 --- a/maas-region/tests/unit/test_charm.py +++ b/maas-region/tests/unit/test_charm.py @@ -119,6 +119,35 @@ def test_ha_proxy_data(self, mock_helper): self.assertEqual(len(ha_data[0]["servers"]), 1) self.assertEqual(ha_data[0]["servers"][0][1], "10.0.0.10") + @patch("charm.MaasHelper", autospec=True) + def test_ha_proxy_data_tls(self, mock_helper): + self.harness.set_leader(True) + self.harness.update_config({"tls_mode": "termination"}) + self.harness.begin() + ha = self.harness.add_relation( + MAAS_API_RELATION, "haproxy", unit_data={"public-address": "proxy.maas"} + ) + + ha_data = yaml.safe_load(self.harness.get_relation_data(ha, "maas-region/0")["services"]) + self.assertEqual(len(ha_data), 2) + self.assertIn("service_name", ha_data[1]) # codespell:ignore + self.assertIn("service_host", ha_data[1]) # codespell:ignore + self.assertEqual(len(ha_data[1]["servers"]), 1) + self.assertEqual(ha_data[1]["servers"][0][1], "10.0.0.10") + + @patch("charm.MaasHelper", autospec=True) + def test_invalid_tls_mode(self, mock_helper): + self.harness.set_leader(True) + self.harness.begin() + ha = self.harness.add_relation( + MAAS_API_RELATION, "haproxy", unit_data={"public-address": "proxy.maas"} + ) + with self.assertRaises(ValueError): + self.harness.update_config({"tls_mode": "invalid_mode"}) + + ha_data = yaml.safe_load(self.harness.get_relation_data(ha, "maas-region/0")["services"]) + self.assertEqual(len(ha_data), 1) + @patch("charm.MaasHelper", autospec=True) def test_on_maas_cluster_changed_new_agent(self, mock_helper): mock_helper.get_maas_mode.return_value = "region"