Skip to content

Commit

Permalink
feat: add configuration entry for TLS termination at HA Proxy (#194) (#…
Browse files Browse the repository at this point in the history
…219)

adds a tls_mode configuration option, which determines how HA Proxy's services yaml configuration should be updated

(cherry picked from commit ac0ba69)

Co-authored-by: Wyatt Rees <[email protected]>
  • Loading branch information
skatsaounis and wyattrees authored Sep 2, 2024
1 parent 8fb706a commit 2ad9a4b
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 2 deletions.
7 changes: 7 additions & 0 deletions maas-region/charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
35 changes: 34 additions & 1 deletion maas-region/src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
55 changes: 54 additions & 1 deletion maas-region/tests/integration/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

import asyncio
import logging
import time
from pathlib import Path
from subprocess import check_output

import pytest
import yaml
Expand All @@ -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
),
Expand Down Expand Up @@ -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
29 changes: 29 additions & 0 deletions maas-region/tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 2ad9a4b

Please sign in to comment.