Skip to content

Commit

Permalink
fix: expose port 10022, wait for service before ActiveStatus (#11)
Browse files Browse the repository at this point in the history
* fix: expose port 10022

* chore: prefer return over break

* test: test for open port
  • Loading branch information
yanksyoon authored Jan 25, 2024
1 parent b801b09 commit 01c7023
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 8 deletions.
10 changes: 5 additions & 5 deletions src-docs/tmate.py.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Configurations and functions to operate tmate-ssh-server.

---

<a href="../src/tmate.py#L99"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
<a href="../src/tmate.py#L102"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

## <kbd>function</kbd> `install_dependencies`

Expand All @@ -41,7 +41,7 @@ Install dependenciese required to start tmate-ssh-server container.

---

<a href="../src/tmate.py#L116"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
<a href="../src/tmate.py#L119"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

## <kbd>function</kbd> `install_keys`

Expand All @@ -66,7 +66,7 @@ Install key creation script and generate keys.

---

<a href="../src/tmate.py#L141"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
<a href="../src/tmate.py#L169"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

## <kbd>function</kbd> `start_daemon`

Expand All @@ -91,7 +91,7 @@ Install unit files and start daemon.

---

<a href="../src/tmate.py#L192"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
<a href="../src/tmate.py#L223"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

## <kbd>function</kbd> `get_fingerprints`

Expand All @@ -115,7 +115,7 @@ Get fingerprint from generated keys.

---

<a href="../src/tmate.py#L216"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
<a href="../src/tmate.py#L247"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

## <kbd>function</kbd> `generate_tmate_conf`

Expand Down
1 change: 1 addition & 0 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def _on_install(self, event: ops.InstallEvent) -> None:
logger.error("Something went wrong initializing keys, %s.", exc)
raise

self.unit.open_port("tcp", tmate.PORT)
self.sshdebug.update_relation_data(host=str(self.state.ip_addr), fingerprints=fingerprints)
self.unit.status = ops.ActiveStatus()

Expand Down
31 changes: 31 additions & 0 deletions src/tmate.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
import subprocess # nosec
import textwrap
import typing
from datetime import datetime, timedelta
from functools import partial
from pathlib import Path
from time import sleep

import jinja2
from charms.operator_libs_linux.v0 import apt, passwd
Expand Down Expand Up @@ -138,6 +141,31 @@ def install_keys(host_ip: typing.Union[ipaddress.IPv4Address, ipaddress.IPv6Addr
raise KeyInstallError from exc


def _wait_for(
func: typing.Callable[[], typing.Any], timeout: int = 300, check_interval: int = 10
) -> None:
"""Wait for function execution to become truthy.
Args:
func: A callback function to wait to return a truthy value.
timeout: Time in seconds to wait for function result to become truthy.
check_interval: Time in seconds to wait between ready checks.
Raises:
TimeoutError: if the callback function did not return a truthy value within timeout.
"""
start_time = now = datetime.now()
min_wait_seconds = timedelta(seconds=timeout)
while now - start_time < min_wait_seconds:
if func():
return
now = datetime.now()
sleep(check_interval)
if func():
return
raise TimeoutError()


def start_daemon(address: str) -> None:
"""Install unit files and start daemon.
Expand All @@ -158,8 +186,11 @@ def start_daemon(address: str) -> None:
try:
systemd.daemon_reload()
systemd.service_start(TMATE_SERVICE_NAME)
_wait_for(partial(systemd.service_running, TMATE_SERVICE_NAME), timeout=60 * 10)
except systemd.SystemdError as exc:
raise DaemonStartError("Failed to start tmate-ssh-server daemon.") from exc
except TimeoutError as exc:
raise DaemonStartError("Timed out waiting for tmate service to start.") from exc


@dataclasses.dataclass
Expand Down
3 changes: 2 additions & 1 deletion tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def test__on_install(
"""
arrange: given a monkeypatched tmate installation function calls.
act: when _on_install is called.
assert: the unit is in active status.
assert: the unit is in active status and tmate ssh server port is opened.
"""
monkeypatch.setattr(tmate, "install_dependencies", MagicMock(spec=tmate.install_dependencies))
monkeypatch.setattr(tmate, "install_keys", MagicMock(spec=tmate.install_keys))
Expand All @@ -132,4 +132,5 @@ def test__on_install(
mock_event = MagicMock(spec=ops.InstallEvent)
charm._on_install(mock_event)

assert ops.Port(protocol="tcp", port=tmate.PORT) in charm.unit.opened_ports()
assert charm.unit.status.name == "active"
74 changes: 72 additions & 2 deletions tests/unit/test_tmate.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,45 @@ def test_install_keys(monkeypatch: pytest.MonkeyPatch):
tmate.install_keys(MagicMock())


def test__wait_for_timeout_error(monkeypatch: pytest.MonkeyPatch):
"""
arrange: given a mock function that returns Falsy value.
act: when _wait_for function is called.
assert: TimeoutError is raised.
"""
monkeypatch.setattr(tmate, "sleep", MagicMock(spec=tmate.sleep)) # to speed up testing
mock_func = MagicMock(return_value=False)

with pytest.raises(TimeoutError):
tmate._wait_for(mock_func, timeout=2, check_interval=1)


@pytest.mark.parametrize(
"timeout, interval",
[
pytest.param(
3,
1,
id="within interval",
),
pytest.param(0, 1, id="last check"),
],
)
def test__wait_for(monkeypatch: pytest.MonkeyPatch, timeout: int, interval: int):
"""
arrange: given a mock function that returns Truthy value.
act: when _wait_for function is called.
assert: the function returns without raising an error.
"""
monkeypatch.setattr(tmate, "sleep", MagicMock(spec=tmate.sleep)) # to speed up testing
mock_func = MagicMock(return_value=True)

tmate._wait_for(mock_func, timeout=timeout, check_interval=interval)


def test_start_daemon_daemon_reload_error(monkeypatch: pytest.MonkeyPatch):
"""
arrange: given a monkeypatched subprocess call that raises CalledProcessError.
arrange: given a monkeypatched systemd call that raises SystemdError.
act: when start_daemon is called.
assert: DaemonStartError is raised.
"""
Expand All @@ -159,9 +195,43 @@ def test_start_daemon_daemon_reload_error(monkeypatch: pytest.MonkeyPatch):
),
)

with pytest.raises(tmate.DaemonStartError):
with pytest.raises(tmate.DaemonStartError) as exc:
tmate.start_daemon(address="test")

assert "Failed to start tmate-ssh-server daemon." in str(exc.value)


def test_start_daemon_service_timeout_error(monkeypatch: pytest.MonkeyPatch):
"""
arrange: given a monkeypatched _wait_for systemd service all that raises a timeout error.
act: when start_daemon is called.
assert: DaemonStartError is raised.
"""
monkeypatch.setattr(tmate, "WORK_DIR", MagicMock(spec=Path))
monkeypatch.setattr(tmate, "KEYS_DIR", MagicMock(spec=Path))
monkeypatch.setattr(tmate, "CREATE_KEYS_SCRIPT_PATH", MagicMock(spec=Path))
monkeypatch.setattr(tmate, "TMATE_SSH_SERVER_SERVICE_PATH", MagicMock(spec=Path))
monkeypatch.setattr(
tmate.systemd,
"daemon_reload",
MagicMock(spec=tmate.systemd.daemon_reload),
)
monkeypatch.setattr(
tmate.systemd,
"service_start",
MagicMock(spec=tmate.systemd.service_start),
)
monkeypatch.setattr(
tmate,
"_wait_for",
MagicMock(spec=tmate._wait_for, side_effect=TimeoutError),
)

with pytest.raises(tmate.DaemonStartError) as exc:
tmate.start_daemon(address="test")

assert "Timed out waiting for tmate service to start." in str(exc.value)


def test_start_daemon_service_start_error(monkeypatch: pytest.MonkeyPatch):
"""
Expand Down

0 comments on commit 01c7023

Please sign in to comment.