POST /deckies/deploy now branches on DECNET_MODE + enrolled host presence: when the caller is a master with at least one reachable swarm host, round- robin host_uuids are assigned over new deckies and the config is dispatched via AgentClient. Falls back to local docker-compose otherwise. Extracts the dispatch loop from api_deploy_swarm into dispatch_decnet_config so both endpoints share the same shard/dispatch/persist path. Adds GET /system/deployment-mode for the UI to show 'will shard across N hosts' vs 'will deploy locally' before the operator clicks deploy.
106 lines
3.7 KiB
Python
106 lines
3.7 KiB
Python
"""POST /deckies/deploy auto-mode: master + swarm hosts → shard to workers."""
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import patch, AsyncMock
|
|
|
|
import pytest
|
|
|
|
from decnet.web.dependencies import repo
|
|
from decnet.web.db.models import SwarmDeployResponse, SwarmHostResult
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def contract_test_mode(monkeypatch):
|
|
monkeypatch.setenv("DECNET_CONTRACT_TEST", "true")
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def mock_network():
|
|
with patch("decnet.web.router.fleet.api_deploy_deckies.get_host_ip", return_value="192.168.1.100"):
|
|
with patch("decnet.web.router.fleet.api_deploy_deckies.detect_interface", return_value="eth0"):
|
|
with patch("decnet.web.router.fleet.api_deploy_deckies.detect_subnet", return_value=("192.168.1.0/24", "192.168.1.1")):
|
|
yield
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_deploy_automode_unihost_when_no_swarm_hosts(client, auth_token, monkeypatch):
|
|
"""No swarm hosts enrolled → local unihost deploy."""
|
|
monkeypatch.setenv("DECNET_MODE", "master")
|
|
for row in await repo.list_swarm_hosts():
|
|
await repo.delete_swarm_host(row["uuid"])
|
|
await repo.set_state("deployment", None)
|
|
|
|
ini = "[decky-solo]\nservices = ssh\n"
|
|
resp = await client.post(
|
|
"/api/v1/deckies/deploy",
|
|
json={"ini_content": ini},
|
|
headers={"Authorization": f"Bearer {auth_token}"},
|
|
)
|
|
assert resp.status_code == 200, resp.text
|
|
assert resp.json()["mode"] == "unihost"
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_deploy_automode_shards_when_swarm_host_enrolled(client, auth_token, monkeypatch):
|
|
"""Master + one active swarm host → swarm mode, dispatch invoked."""
|
|
monkeypatch.setenv("DECNET_MODE", "master")
|
|
await repo.set_state("deployment", None)
|
|
|
|
for row in await repo.list_swarm_hosts():
|
|
await repo.delete_swarm_host(row["uuid"])
|
|
|
|
from datetime import datetime, timezone
|
|
await repo.add_swarm_host({
|
|
"uuid": "host-A",
|
|
"name": "worker-a",
|
|
"address": "10.0.0.50",
|
|
"agent_port": 8765,
|
|
"status": "active",
|
|
"client_cert_fingerprint": "x" * 64,
|
|
"updater_cert_fingerprint": None,
|
|
"cert_bundle_path": "/tmp/worker-a",
|
|
"enrolled_at": datetime.now(timezone.utc),
|
|
"notes": "",
|
|
})
|
|
|
|
fake_response = SwarmDeployResponse(results=[
|
|
SwarmHostResult(host_uuid="host-A", host_name="worker-a", ok=True, detail={})
|
|
])
|
|
|
|
with patch(
|
|
"decnet.web.router.fleet.api_deploy_deckies.dispatch_decnet_config",
|
|
new=AsyncMock(return_value=fake_response),
|
|
) as mock_dispatch:
|
|
ini = "[decky-01]\nservices = ssh\n[decky-02]\nservices = http\n"
|
|
resp = await client.post(
|
|
"/api/v1/deckies/deploy",
|
|
json={"ini_content": ini},
|
|
headers={"Authorization": f"Bearer {auth_token}"},
|
|
)
|
|
|
|
assert resp.status_code == 200, resp.text
|
|
assert resp.json()["mode"] == "swarm"
|
|
assert mock_dispatch.await_count == 1
|
|
dispatched_config = mock_dispatch.await_args.args[0]
|
|
assert dispatched_config.mode == "swarm"
|
|
assert all(d.host_uuid == "host-A" for d in dispatched_config.deckies)
|
|
|
|
await repo.delete_swarm_host("host-A")
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_deployment_mode_endpoint(client, auth_token, monkeypatch):
|
|
monkeypatch.setenv("DECNET_MODE", "master")
|
|
for row in await repo.list_swarm_hosts():
|
|
await repo.delete_swarm_host(row["uuid"])
|
|
|
|
resp = await client.get(
|
|
"/api/v1/system/deployment-mode",
|
|
headers={"Authorization": f"Bearer {auth_token}"},
|
|
)
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert body["role"] == "master"
|
|
assert body["mode"] == "unihost"
|
|
assert body["swarm_host_count"] == 0
|