2024-11-06 14:18:51 +00:00
|
|
|
from os import path
|
2024-11-03 21:57:12 +00:00
|
|
|
from typing import Annotated
|
2024-11-03 16:55:10 +00:00
|
|
|
|
2024-11-03 19:42:55 +00:00
|
|
|
from docker import errors, from_env
|
2024-11-03 20:24:49 +00:00
|
|
|
from docker.models.containers import Container
|
2024-11-03 16:55:10 +00:00
|
|
|
from dotenv import load_dotenv
|
2024-11-06 14:18:51 +00:00
|
|
|
from fastapi import Depends, FastAPI, HTTPException
|
2024-11-04 15:55:23 +00:00
|
|
|
from fastapi.responses import PlainTextResponse
|
2024-11-06 14:18:51 +00:00
|
|
|
from fastapi.security import HTTPBasicCredentials
|
|
|
|
from starlette import status
|
2024-11-03 16:55:10 +00:00
|
|
|
from uvicorn import run
|
|
|
|
|
2024-11-06 14:18:51 +00:00
|
|
|
from .security import AuthStaticFiles, check_auth, security
|
|
|
|
from .types import ContainerRequest, SerializedContainer
|
|
|
|
from .utils import serialize_container
|
|
|
|
|
2024-11-03 16:55:10 +00:00
|
|
|
load_dotenv()
|
|
|
|
client = from_env()
|
|
|
|
app = FastAPI(dependencies=[Depends(check_auth)])
|
|
|
|
|
|
|
|
|
2024-11-04 10:04:25 +00:00
|
|
|
def select_container(
|
|
|
|
container_name: str, credentials: Annotated[HTTPBasicCredentials, Depends(security)]
|
|
|
|
) -> Container:
|
|
|
|
try:
|
|
|
|
container = client.containers.get(container_name)
|
|
|
|
except errors.APIError:
|
|
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
|
|
|
|
|
|
if (
|
|
|
|
credentials.username != "admin"
|
|
|
|
and container.labels.get("engine") != "pilotwings"
|
|
|
|
and container.labels.get("owner") != credentials.username
|
|
|
|
):
|
|
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
|
|
|
|
|
|
|
return container
|
|
|
|
|
|
|
|
|
2024-11-03 16:55:10 +00:00
|
|
|
@app.get("/api/containers")
|
|
|
|
def get_containers(
|
|
|
|
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
|
2024-11-03 21:57:12 +00:00
|
|
|
) -> list[SerializedContainer]:
|
2024-11-03 16:55:10 +00:00
|
|
|
if credentials.username == "admin":
|
2024-11-03 20:24:49 +00:00
|
|
|
return [
|
2024-11-04 10:04:25 +00:00
|
|
|
serialize_container(container)
|
|
|
|
for container in client.containers.list(
|
2024-11-06 14:18:51 +00:00
|
|
|
all=True, filters={"label": ["engine=pilotwings"]}
|
2024-11-04 10:04:25 +00:00
|
|
|
)
|
2024-11-03 20:24:49 +00:00
|
|
|
]
|
2024-11-03 16:55:10 +00:00
|
|
|
|
|
|
|
return [
|
2024-11-03 20:24:49 +00:00
|
|
|
serialize_container(container)
|
2024-11-03 16:55:10 +00:00
|
|
|
for container in client.containers.list(
|
2024-11-05 22:45:23 +00:00
|
|
|
all=True,
|
2024-11-04 10:04:25 +00:00
|
|
|
filters={"label": ["engine=pilotwings", f"owner={credentials.username}"]},
|
2024-11-03 16:55:10 +00:00
|
|
|
)
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2024-11-03 19:42:55 +00:00
|
|
|
@app.get("/api/container/{container_name}")
|
|
|
|
def get_container(
|
|
|
|
container_name: str,
|
|
|
|
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
|
2024-11-03 21:57:12 +00:00
|
|
|
) -> SerializedContainer:
|
2024-11-04 10:04:25 +00:00
|
|
|
return serialize_container(select_container(container_name, credentials))
|
2024-11-03 21:57:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
@app.post("/api/container/{container_name}")
|
2024-11-04 10:04:25 +00:00
|
|
|
def create_or_update_container(
|
2024-11-03 21:57:12 +00:00
|
|
|
container_name: str,
|
2024-11-04 10:04:25 +00:00
|
|
|
request_body: ContainerRequest,
|
2024-11-03 21:57:12 +00:00
|
|
|
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
|
|
|
|
) -> SerializedContainer:
|
2024-11-06 21:20:30 +00:00
|
|
|
owner = None
|
2024-11-04 10:04:25 +00:00
|
|
|
networks = client.networks.list(names=["pilotwings"])
|
2024-11-03 21:57:12 +00:00
|
|
|
|
2024-11-04 10:04:25 +00:00
|
|
|
if not networks:
|
|
|
|
client.networks.create("pilotwings")
|
2024-11-03 21:57:12 +00:00
|
|
|
|
2024-11-04 15:55:23 +00:00
|
|
|
client.images.pull(request_body.image)
|
|
|
|
|
2024-11-03 21:57:12 +00:00
|
|
|
try:
|
2024-11-06 21:20:30 +00:00
|
|
|
container = select_container(container_name, credentials)
|
|
|
|
owner = container.labels.get("owner")
|
2024-11-04 15:55:23 +00:00
|
|
|
delete_container(container_name, credentials)
|
|
|
|
except HTTPException:
|
2024-11-04 10:04:25 +00:00
|
|
|
pass
|
2024-11-03 21:57:12 +00:00
|
|
|
|
2024-11-06 21:20:30 +00:00
|
|
|
container = client.containers.run(
|
|
|
|
request_body.image,
|
|
|
|
detach=True,
|
|
|
|
environment=request_body.environment,
|
|
|
|
labels={"engine": "pilotwings", "owner": owner or credentials.username},
|
|
|
|
name=container_name,
|
|
|
|
network="pilotwings",
|
2024-11-09 11:52:18 +00:00
|
|
|
restart_policy={"Name": "on-failure"},
|
2024-11-03 21:57:12 +00:00
|
|
|
)
|
|
|
|
|
2024-11-06 14:18:51 +00:00
|
|
|
client.images.prune({"dangling": True})
|
|
|
|
|
2024-11-06 21:20:30 +00:00
|
|
|
return serialize_container(container)
|
2024-11-06 14:18:51 +00:00
|
|
|
|
2024-11-03 21:57:12 +00:00
|
|
|
|
2024-11-04 15:55:23 +00:00
|
|
|
@app.post("/api/container/{container_name}/pull")
|
|
|
|
def pull_container(
|
|
|
|
container_name: str,
|
|
|
|
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
|
|
|
|
) -> SerializedContainer:
|
|
|
|
container = select_container(container_name, credentials)
|
|
|
|
|
|
|
|
if not container.image:
|
|
|
|
raise HTTPException(status_code=status.HTTP_410_GONE)
|
|
|
|
|
|
|
|
request_body = ContainerRequest(
|
|
|
|
image=container.image.tags[0], environment=container.attrs["Config"]["Env"]
|
|
|
|
)
|
|
|
|
|
|
|
|
return create_or_update_container(container_name, request_body, credentials)
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/api/container/{container_name}/restart")
|
|
|
|
def restart_container(
|
|
|
|
container_name: str,
|
|
|
|
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
|
|
|
|
) -> SerializedContainer:
|
|
|
|
container = select_container(container_name, credentials)
|
|
|
|
container.restart()
|
2024-11-05 22:45:23 +00:00
|
|
|
container.reload()
|
|
|
|
return serialize_container(container)
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/api/container/{container_name}/start")
|
|
|
|
def start_container(
|
|
|
|
container_name: str,
|
|
|
|
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
|
|
|
|
) -> SerializedContainer:
|
|
|
|
container = select_container(container_name, credentials)
|
|
|
|
container.start()
|
|
|
|
container.reload()
|
|
|
|
return serialize_container(container)
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/api/container/{container_name}/stop")
|
|
|
|
def stop_container(
|
|
|
|
container_name: str,
|
|
|
|
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
|
|
|
|
) -> SerializedContainer:
|
|
|
|
container = select_container(container_name, credentials)
|
|
|
|
container.stop()
|
|
|
|
container.reload()
|
2024-11-04 15:55:23 +00:00
|
|
|
return serialize_container(container)
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/api/container/{container_name}/logs", response_class=PlainTextResponse)
|
|
|
|
def get_container_logs(
|
|
|
|
container_name: str,
|
|
|
|
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
|
|
|
|
) -> str:
|
|
|
|
container = select_container(container_name, credentials)
|
|
|
|
return container.logs().decode()
|
|
|
|
|
|
|
|
|
|
|
|
@app.delete("/api/container/{container_name}")
|
|
|
|
def delete_container(
|
|
|
|
container_name: str,
|
|
|
|
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
|
|
|
|
) -> None:
|
|
|
|
container = select_container(container_name, credentials)
|
|
|
|
container.stop()
|
|
|
|
container.remove(v=True, force=True)
|
2024-11-06 14:18:51 +00:00
|
|
|
client.images.prune({"dangling": True})
|
2024-11-03 22:10:21 +00:00
|
|
|
|
|
|
|
|
2024-11-03 16:55:10 +00:00
|
|
|
app.mount(
|
|
|
|
"/",
|
|
|
|
AuthStaticFiles(
|
|
|
|
directory=f"{path.dirname(path.realpath(__file__))}/dist", html=True
|
|
|
|
),
|
|
|
|
name="static",
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-11-03 20:24:49 +00:00
|
|
|
def launch() -> None:
|
2024-11-03 16:55:10 +00:00
|
|
|
run(app, host="0.0.0.0")
|