mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 02:48:38 -04:00
- Replace nginx with Caddy as the web server for both Docker and native (RPi/Debian) deployments. Caddy handles HTTPS termination, reverse proxying to all services (API, React app, Controller, Kiwix, Help, Map), and serves the media/download file browsers. - Generate a Root CA and signed leaf certificates for HTTPS. The Root CA uses Name Constraints to restrict signing to local addresses only (.local, localhost, and RFC 1918 private IP ranges), mitigating MITM risk if the CA key is compromised. - Add an HTTP landing page that guides users through one-time CA certificate installation on iOS, Android, macOS, Windows, Linux, and Firefox, with auto-redirect when the cert is already trusted. - Add trailing-slash redirects for /controller, /docs, /media, and /download so bare paths don't fall through to the React catch-all. - Add a Root CA download link on the Settings page under Configs. - Update help.sh diagnostics for IPv6 listener detection and HTTP/2 status codes. - Remove all nginx config files and the https_proxy Docker container. - Bump wrolpi-help submodule with certificate documentation.
240 lines
7.3 KiB
Python
240 lines
7.3 KiB
Python
"""
|
|
Docker container management for WROLPi Controller.
|
|
|
|
Used when running in Docker mode instead of systemd.
|
|
"""
|
|
|
|
import os
|
|
|
|
from controller.lib.config import is_docker_mode
|
|
|
|
# Import docker only if available (optional dependency)
|
|
try:
|
|
import docker
|
|
from docker.errors import NotFound, APIError
|
|
|
|
DOCKER_AVAILABLE = True
|
|
except ImportError:
|
|
DOCKER_AVAILABLE = False
|
|
docker = None
|
|
NotFound = Exception
|
|
APIError = Exception
|
|
|
|
# Container name prefix (from docker-compose project)
|
|
CONTAINER_PREFIX = os.environ.get("COMPOSE_PROJECT_NAME", "wrolpi")
|
|
|
|
# Containers that should not have a "View" button (non-HTTP services)
|
|
NON_VIEWABLE_CONTAINERS = {"db", "postgres", "postgresql", "redis", "memcached"}
|
|
|
|
# Containers that should use HTTPS
|
|
HTTPS_CONTAINERS = {"web"}
|
|
|
|
|
|
def can_manage_containers() -> bool:
|
|
"""Check if we can manage Docker containers."""
|
|
if not is_docker_mode():
|
|
return False
|
|
if not DOCKER_AVAILABLE:
|
|
return False
|
|
if not os.path.exists("/var/run/docker.sock"):
|
|
return False
|
|
return True
|
|
|
|
|
|
def _get_client():
|
|
"""Get Docker client."""
|
|
if not DOCKER_AVAILABLE:
|
|
raise RuntimeError("Docker library not installed")
|
|
return docker.from_env()
|
|
|
|
|
|
def _get_container_name(service_name: str) -> str:
|
|
"""Convert service name to container name."""
|
|
# docker-compose names: {project}-{service}-{number}
|
|
return f"{CONTAINER_PREFIX}-{service_name}-1"
|
|
|
|
|
|
def get_container_status(name: str) -> dict:
|
|
"""
|
|
Get status of a Docker container.
|
|
|
|
Args:
|
|
name: Service name
|
|
|
|
Returns:
|
|
dict with status info
|
|
"""
|
|
if not can_manage_containers():
|
|
return {
|
|
"name": name,
|
|
"status": "unknown",
|
|
"available": False,
|
|
"reason": "Docker management not available",
|
|
}
|
|
|
|
try:
|
|
client = _get_client()
|
|
container_name = _get_container_name(name)
|
|
container = client.containers.get(container_name)
|
|
|
|
# Map Docker status to our status
|
|
docker_status = container.status
|
|
if docker_status == "running":
|
|
status = "running"
|
|
elif docker_status == "exited":
|
|
status = "stopped"
|
|
elif docker_status == "restarting":
|
|
status = "restarting"
|
|
else:
|
|
status = docker_status
|
|
|
|
# Get port info from container
|
|
ports = container.attrs.get("NetworkSettings", {}).get("Ports", {})
|
|
port = None
|
|
for container_port, host_bindings in ports.items():
|
|
if host_bindings:
|
|
port = int(host_bindings[0].get("HostPort", 0))
|
|
break
|
|
|
|
return {
|
|
"name": name,
|
|
"container_name": container_name,
|
|
"status": status,
|
|
"docker_status": docker_status,
|
|
"port": port,
|
|
"available": True,
|
|
"viewable": name not in NON_VIEWABLE_CONTAINERS,
|
|
"use_https": name.endswith("_https") or name in HTTPS_CONTAINERS,
|
|
}
|
|
except NotFound:
|
|
return {
|
|
"name": name,
|
|
"status": "not_found",
|
|
"available": True,
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"name": name,
|
|
"status": "error",
|
|
"error": str(e),
|
|
"available": False,
|
|
}
|
|
|
|
|
|
def get_all_containers_status() -> list[dict]:
|
|
"""
|
|
Get status of all WROLPi containers.
|
|
|
|
Returns:
|
|
list of container status dicts
|
|
"""
|
|
if not can_manage_containers():
|
|
return []
|
|
|
|
try:
|
|
client = _get_client()
|
|
containers = client.containers.list(all=True)
|
|
|
|
results = []
|
|
for container in containers:
|
|
if container.name.startswith(CONTAINER_PREFIX):
|
|
# Extract service name from container name
|
|
parts = container.name.split("-")
|
|
if len(parts) >= 2:
|
|
service_name = parts[1]
|
|
else:
|
|
service_name = container.name
|
|
|
|
# Get port info from container
|
|
ports = container.attrs.get("NetworkSettings", {}).get("Ports", {})
|
|
port = None
|
|
for container_port, host_bindings in ports.items():
|
|
if host_bindings:
|
|
port = int(host_bindings[0].get("HostPort", 0))
|
|
break
|
|
|
|
results.append({
|
|
"name": service_name,
|
|
"container_name": container.name,
|
|
"status": "running" if container.status == "running" else "stopped",
|
|
"docker_status": container.status,
|
|
"port": port,
|
|
"viewable": service_name not in NON_VIEWABLE_CONTAINERS,
|
|
"use_https": service_name.endswith("_https") or service_name in HTTPS_CONTAINERS,
|
|
})
|
|
|
|
return results
|
|
except Exception as e:
|
|
return [{"error": str(e)}]
|
|
|
|
|
|
def start_container(name: str) -> dict:
|
|
"""Start a Docker container."""
|
|
if not can_manage_containers():
|
|
return {"success": False, "error": "Docker management not available"}
|
|
|
|
try:
|
|
client = _get_client()
|
|
container_name = _get_container_name(name)
|
|
container = client.containers.get(container_name)
|
|
container.start()
|
|
return {"success": True, "service": name, "action": "start"}
|
|
except NotFound:
|
|
return {"success": False, "error": f"Container not found: {name}"}
|
|
except Exception as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
def stop_container(name: str) -> dict:
|
|
"""Stop a Docker container."""
|
|
if not can_manage_containers():
|
|
return {"success": False, "error": "Docker management not available"}
|
|
|
|
try:
|
|
client = _get_client()
|
|
container_name = _get_container_name(name)
|
|
container = client.containers.get(container_name)
|
|
container.stop(timeout=30)
|
|
return {"success": True, "service": name, "action": "stop"}
|
|
except NotFound:
|
|
return {"success": False, "error": f"Container not found: {name}"}
|
|
except Exception as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
def restart_container(name: str) -> dict:
|
|
"""Restart a Docker container."""
|
|
if not can_manage_containers():
|
|
return {"success": False, "error": "Docker management not available"}
|
|
|
|
try:
|
|
client = _get_client()
|
|
container_name = _get_container_name(name)
|
|
container = client.containers.get(container_name)
|
|
container.restart(timeout=30)
|
|
return {"success": True, "service": name, "action": "restart"}
|
|
except NotFound:
|
|
return {"success": False, "error": f"Container not found: {name}"}
|
|
except Exception as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
def get_container_logs(name: str, lines: int = 100) -> dict:
|
|
"""Get logs from a Docker container."""
|
|
if not can_manage_containers():
|
|
return {"error": "Docker management not available"}
|
|
|
|
try:
|
|
client = _get_client()
|
|
container_name = _get_container_name(name)
|
|
container = client.containers.get(container_name)
|
|
logs = container.logs(tail=lines).decode("utf-8")
|
|
return {
|
|
"service": name,
|
|
"lines": lines,
|
|
"logs": logs,
|
|
}
|
|
except NotFound:
|
|
return {"error": f"Container not found: {name}"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|