feat: add graceful shutdown endpoint for UVM API server and update documentation
This commit is contained in:
+39
-8
@@ -7,7 +7,7 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from uvm.cli import build_parser, main
|
||||
from uvm.config import Settings
|
||||
@@ -99,19 +99,31 @@ class ServerCliTests(unittest.TestCase):
|
||||
application = fake_application(api_token="test-token")
|
||||
application.settings.api_tls_cert = certificate
|
||||
application.settings.api_tls_key = key
|
||||
fake_uvicorn = SimpleNamespace(run=lambda *_args, **_kwargs: None)
|
||||
api = SimpleNamespace(state=SimpleNamespace())
|
||||
server = SimpleNamespace(run=Mock())
|
||||
config = object()
|
||||
fake_uvicorn = SimpleNamespace(
|
||||
Config=Mock(return_value=config),
|
||||
Server=Mock(return_value=server),
|
||||
)
|
||||
|
||||
with (
|
||||
patch("uvm.server._validate_server_settings"),
|
||||
patch("uvm.server.create_api", return_value=object()),
|
||||
patch("uvm.server.create_api", return_value=api),
|
||||
patch.dict(sys.modules, {"uvicorn": fake_uvicorn}),
|
||||
patch.object(fake_uvicorn, "run") as run_mock,
|
||||
):
|
||||
run_server(application, host="0.0.0.0", port=8443)
|
||||
|
||||
run_mock.assert_called_once()
|
||||
self.assertEqual(run_mock.call_args.kwargs["ssl_certfile"], str(certificate))
|
||||
self.assertEqual(run_mock.call_args.kwargs["ssl_keyfile"], str(key))
|
||||
fake_uvicorn.Config.assert_called_once_with(
|
||||
api,
|
||||
host="0.0.0.0",
|
||||
port=8443,
|
||||
ssl_certfile=str(certificate),
|
||||
ssl_keyfile=str(key),
|
||||
)
|
||||
fake_uvicorn.Server.assert_called_once_with(config)
|
||||
self.assertIs(api.state.uvicorn_server, server)
|
||||
server.run.assert_called_once_with()
|
||||
|
||||
def test_api_token_rejects_non_ascii_or_whitespace(self) -> None:
|
||||
for token in ("s\u00e9cret", "contains space", ""):
|
||||
@@ -300,7 +312,8 @@ class ApiRouterTests(unittest.TestCase):
|
||||
)
|
||||
application.state_store = SimpleNamespace(initialize=lambda: None)
|
||||
self.headers = {"X-UVM-Token": "test-token"}
|
||||
self.client = TestClient(create_api(application, host="127.0.0.1"))
|
||||
self.api = create_api(application, host="127.0.0.1")
|
||||
self.client = TestClient(self.api)
|
||||
self.addCleanup(self.client.close)
|
||||
|
||||
def test_health_is_available_without_credentials(self) -> None:
|
||||
@@ -339,6 +352,7 @@ class ApiRouterTests(unittest.TestCase):
|
||||
("POST", "/vms/test-vm/ports", {}),
|
||||
("POST", "/vms/test-vm/ports/sync", {}),
|
||||
("GET", "/diagnostics", {}),
|
||||
("POST", "/exit", {}),
|
||||
("POST", "/install", {}),
|
||||
):
|
||||
for headers in ({}, {"X-UVM-Token": "wrong-token"}):
|
||||
@@ -353,6 +367,22 @@ class ApiRouterTests(unittest.TestCase):
|
||||
self.assertEqual(authorized.json()[0]["id"], "vm-test")
|
||||
self.assertNotIn("password", authorized.json()[0])
|
||||
|
||||
def test_exit_requests_a_graceful_authenticated_server_shutdown(self) -> None:
|
||||
server = SimpleNamespace(should_exit=False)
|
||||
self.api.state.uvicorn_server = server
|
||||
|
||||
response = self.client.post("/exit", headers=self.headers)
|
||||
|
||||
self.assertEqual(response.status_code, 202)
|
||||
self.assertEqual(response.json(), {"status": "stopping"})
|
||||
self.assertTrue(server.should_exit)
|
||||
|
||||
def test_exit_requires_a_running_uvicorn_server(self) -> None:
|
||||
response = self.client.post("/exit", headers=self.headers)
|
||||
|
||||
self.assertEqual(response.status_code, 503)
|
||||
self.assertEqual(response.json(), {"detail": "server shutdown is unavailable outside uvm --serve"})
|
||||
|
||||
def test_vm_lifecycle_routes_and_extra_field_validation(self) -> None:
|
||||
created = self.client.post(
|
||||
"/vms",
|
||||
@@ -752,6 +782,7 @@ class ApiRouterTests(unittest.TestCase):
|
||||
("/", "get"),
|
||||
("/dashboard", "get"),
|
||||
("/health", "get"),
|
||||
("/exit", "post"),
|
||||
("/install", "post"),
|
||||
("/vms", "get"),
|
||||
("/vms", "post"),
|
||||
|
||||
Reference in New Issue
Block a user