- Live remote-stack logs over a WebSocket proxied through the central app to
the agent (/ws/agent-logs/{agent}/{stack}); agent gains a WS log endpoint.
- Deploy to a remote host from the UI: host selector in the New Stack editor
and template dialog; templates instantiate onto an agent via the proxy.
- Network attach/detach: expandable inspect view per network with
connect/disconnect + container picker; GET /{id}/containers, POST connect/disconnect.
- Remove dead pages/Placeholder.tsx.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
154 lines
4.2 KiB
Python
154 lines
4.2 KiB
Python
"""Docker network management endpoints."""
|
|
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
from pydantic import BaseModel
|
|
from sqlmodel import Session
|
|
|
|
from auth import get_current_user, require_admin
|
|
from database import get_session
|
|
from docker_client import DockerError
|
|
from models.user import User
|
|
from services import audit_service, network_service
|
|
|
|
router = APIRouter(prefix="/api/networks", tags=["networks"])
|
|
|
|
_STATUS = {"invalid_request": 400, "forbidden": 403, "not_found": 404}
|
|
|
|
|
|
def _ip(request: Request) -> str:
|
|
return request.client.host if request.client else "unknown"
|
|
|
|
|
|
def _map(exc: DockerError):
|
|
code = _STATUS.get(exc.error)
|
|
if code:
|
|
raise HTTPException(status_code=code, detail=exc.detail or exc.error)
|
|
raise exc # falls through to the global 502 DockerError handler
|
|
|
|
|
|
class NetworkCreate(BaseModel):
|
|
name: str
|
|
driver: str = "bridge"
|
|
subnet: str | None = None
|
|
gateway: str | None = None
|
|
internal: bool = False
|
|
attachable: bool = True
|
|
|
|
|
|
class ContainerRef(BaseModel):
|
|
container: str
|
|
aliases: list[str] | None = None
|
|
force: bool = False
|
|
|
|
|
|
@router.get("")
|
|
def list_networks(_user: User = Depends(get_current_user)) -> list[dict]:
|
|
return network_service.list_networks()
|
|
|
|
|
|
@router.get("/{network_id}")
|
|
def inspect_network(network_id: str, _user: User = Depends(get_current_user)) -> dict:
|
|
try:
|
|
return network_service.inspect_network(network_id)
|
|
except DockerError as exc:
|
|
_map(exc)
|
|
|
|
|
|
@router.get("/{network_id}/containers")
|
|
def network_containers(
|
|
network_id: str, _user: User = Depends(get_current_user)
|
|
) -> list[dict]:
|
|
try:
|
|
return network_service.connectable_containers(network_id)
|
|
except DockerError as exc:
|
|
_map(exc)
|
|
|
|
|
|
@router.post("/{network_id}/connect")
|
|
def connect_container(
|
|
network_id: str,
|
|
body: ContainerRef,
|
|
request: Request,
|
|
session: Session = Depends(get_session),
|
|
user: User = Depends(require_admin),
|
|
) -> dict:
|
|
try:
|
|
network_service.connect_container(network_id, body.container, body.aliases)
|
|
except DockerError as exc:
|
|
_map(exc)
|
|
audit_service.record(
|
|
session, user=user.username, action="network.connect", target=network_id,
|
|
detail=body.container, ip=_ip(request),
|
|
)
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("/{network_id}/disconnect")
|
|
def disconnect_container(
|
|
network_id: str,
|
|
body: ContainerRef,
|
|
request: Request,
|
|
session: Session = Depends(get_session),
|
|
user: User = Depends(require_admin),
|
|
) -> dict:
|
|
try:
|
|
network_service.disconnect_container(network_id, body.container, body.force)
|
|
except DockerError as exc:
|
|
_map(exc)
|
|
audit_service.record(
|
|
session, user=user.username, action="network.disconnect", target=network_id,
|
|
detail=body.container, ip=_ip(request),
|
|
)
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("", status_code=201)
|
|
def create_network(
|
|
body: NetworkCreate,
|
|
request: Request,
|
|
session: Session = Depends(get_session),
|
|
user: User = Depends(require_admin),
|
|
) -> dict:
|
|
try:
|
|
result = network_service.create_network(body.model_dump())
|
|
except DockerError as exc:
|
|
_map(exc)
|
|
audit_service.record(
|
|
session, user=user.username, action="network.create", target=body.name,
|
|
detail=body.driver, ip=_ip(request),
|
|
)
|
|
return result
|
|
|
|
|
|
@router.delete("/{network_id}")
|
|
def delete_network(
|
|
network_id: str,
|
|
request: Request,
|
|
session: Session = Depends(get_session),
|
|
user: User = Depends(require_admin),
|
|
) -> dict:
|
|
try:
|
|
network_service.delete_network(network_id)
|
|
except DockerError as exc:
|
|
_map(exc)
|
|
audit_service.record(
|
|
session, user=user.username, action="network.delete", target=network_id,
|
|
ip=_ip(request),
|
|
)
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("/prune")
|
|
def prune_networks(
|
|
request: Request,
|
|
session: Session = Depends(get_session),
|
|
user: User = Depends(require_admin),
|
|
) -> dict:
|
|
result = network_service.prune_networks()
|
|
audit_service.record(
|
|
session, user=user.username, action="network.prune", target="networks",
|
|
detail=str(result.get("NetworksDeleted") or []), ip=_ip(request),
|
|
)
|
|
return result
|