diff --git a/agent/main.py b/agent/main.py new file mode 100644 index 0000000..23a1721 --- /dev/null +++ b/agent/main.py @@ -0,0 +1,48 @@ +from fastapi import FastAPI, Query +from agent.docker_ops import DockerOps + +app = FastAPI(title="Farm Manager Agent") + +try: + ops = DockerOps() +except Exception: + ops = None # Will be mocked in tests + +@app.get("/health") +def health(): + return ops.get_health() + + +@app.get("/containers") +def list_containers(): + return ops.list_containers() + + +@app.post("/containers/{container_id}/start") +def start_container(container_id: str): + return ops.start_container(container_id) + + +@app.post("/containers/{container_id}/stop") +def stop_container(container_id: str): + return ops.stop_container(container_id) + + +@app.post("/containers/{container_id}/restart") +def restart_container(container_id: str): + return ops.restart_container(container_id) + + +@app.get("/containers/{container_id}/logs") +def get_logs(container_id: str, tail: int = Query(default=200)): + return ops.get_logs(container_id, tail=tail) + + +@app.post("/containers/{container_id}/pull") +def pull_image(container_id: str): + return ops.pull_image(container_id) + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8889) diff --git a/tests/test_agent_api.py b/tests/test_agent_api.py new file mode 100644 index 0000000..5317976 --- /dev/null +++ b/tests/test_agent_api.py @@ -0,0 +1,71 @@ +import pytest +from unittest.mock import patch, MagicMock +from fastapi.testclient import TestClient + + +@pytest.fixture +def mock_ops(): + with patch("agent.main.ops") as mock: + yield mock + + +@pytest.fixture +def client(mock_ops): + from agent.main import app + return TestClient(app) + + +class TestHealthEndpoint: + def test_health_returns_ok(self, client, mock_ops): + mock_ops.get_health.return_value = { + "status": "ok", "hostname": "test-node", "containers_total": 5 + } + resp = client.get("/health") + assert resp.status_code == 200 + assert resp.json()["status"] == "ok" + + +class TestListContainers: + def test_list_containers(self, client, mock_ops): + mock_ops.list_containers.return_value = [{ + "id": "abc123", "name": "test-app", "status": "running", + "image": "nginx:latest", "created": "2026-03-05T00:00:00Z", + "uptime": "2h 30m", "is_swarm": False, "swarm_service": None, + }] + resp = client.get("/containers") + assert resp.status_code == 200 + assert len(resp.json()) == 1 + assert resp.json()[0]["name"] == "test-app" + + +class TestContainerActions: + def test_start(self, client, mock_ops): + mock_ops.start_container.return_value = {"success": True, "message": "Started"} + assert client.post("/containers/abc123/start").json()["success"] is True + + def test_stop(self, client, mock_ops): + mock_ops.stop_container.return_value = {"success": True, "message": "Stopped"} + assert client.post("/containers/abc123/stop").json()["success"] is True + + def test_restart(self, client, mock_ops): + mock_ops.restart_container.return_value = {"success": True, "message": "Restarted"} + assert client.post("/containers/abc123/restart").json()["success"] is True + + +class TestLogs: + def test_get_logs(self, client, mock_ops): + mock_ops.get_logs.return_value = {"container": "test-app", "logs": "line1\nline2"} + resp = client.get("/containers/abc123/logs?tail=50") + assert resp.status_code == 200 + assert "line1" in resp.json()["logs"] + + def test_get_logs_default_tail(self, client, mock_ops): + mock_ops.get_logs.return_value = {"container": "test-app", "logs": "logs"} + client.get("/containers/abc123/logs") + mock_ops.get_logs.assert_called_once_with("abc123", tail=200) + + +class TestPull: + def test_pull(self, client, mock_ops): + mock_ops.pull_image.return_value = {"success": True, "message": "Pulled nginx:latest"} + assert client.post("/containers/abc123/pull").json()["success"] is True