diff --git a/backend/pilotwings.py b/backend/pilotwings.py index c969f69..6095c6d 100644 --- a/backend/pilotwings.py +++ b/backend/pilotwings.py @@ -1,5 +1,5 @@ from os import getenv, path -from typing import Annotated, Any +from typing import Annotated from docker import errors, from_env from docker.models.containers import Container @@ -7,6 +7,7 @@ from dotenv import load_dotenv from fastapi import Depends, FastAPI, HTTPException, Request from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel from starlette import status, types from uvicorn import run @@ -58,24 +59,36 @@ class AuthStaticFiles(StaticFiles): app = FastAPI(dependencies=[Depends(check_auth)]) -def serialize_container(container: Container) -> dict[str, Any]: - return { - "id": container.short_id, - "name": container.name, - "image": container.image.tags[0] if container.image else None, - "labels": container.labels, - "status": container.status, - "health": container.health, - "ports": container.ports, - "owner": container.labels.get("owner"), - "environment": container.attrs["Config"]["Env"], - } +class SerializedContainer(BaseModel): + id: str + name: str | None + image: str | None + labels: dict[str, str] + status: str + health: str + ports: dict[str, str] + owner: str | None + environment: list[str] + + +def serialize_container(container: Container) -> SerializedContainer: + return SerializedContainer( + id=container.short_id, + name=container.name, + image=container.image.tags[0] if container.image else None, + labels=container.labels, + status=container.status, + health=container.health, + ports=container.ports, + owner=container.labels.get("owner"), + environment=container.attrs["Config"]["Env"], + ) @app.get("/api/containers") def get_containers( credentials: Annotated[HTTPBasicCredentials, Depends(security)], -) -> list[dict[str, Any]]: +) -> list[SerializedContainer]: if credentials.username == "admin": return [ serialize_container(container) for container in client.containers.list() @@ -93,7 +106,7 @@ def get_containers( def get_container( container_name: str, credentials: Annotated[HTTPBasicCredentials, Depends(security)], -) -> dict[str, Any]: +) -> SerializedContainer: try: container = client.containers.get(container_name) except errors.APIError: @@ -108,6 +121,67 @@ def get_container( return serialize_container(container) +class UpdateContainerRequest(BaseModel): + environment: dict[str, str] + + +class CreateContainerRequest(UpdateContainerRequest): + image: str + + +@app.post("/api/container/{container_name}") +def create_container( + container_name: str, + request_body: CreateContainerRequest, + credentials: Annotated[HTTPBasicCredentials, Depends(security)], +) -> SerializedContainer: + return serialize_container( + client.containers.run( + request_body.image, + detach=True, + environment=request_body.environment, + labels={"owner": credentials.username}, + name=container_name, + restart_policy={"Name": "always"}, + ) + ) + + +@app.put("/api/container/{container_name}") +def update_container( + container_name: str, + request_body: UpdateContainerRequest, + credentials: Annotated[HTTPBasicCredentials, Depends(security)], +) -> SerializedContainer: + try: + container = client.containers.get(container_name) + except errors.APIError: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + if ( + credentials.username != "admin" + and f"owner={credentials.username}" not in container.labels + ): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + + if not container.image: + raise HTTPException(status_code=status.HTTP_410_GONE) + + container.stop() + container.remove(v=True, force=True) + + return serialize_container( + client.containers.run( + container.image.tags[0], + detach=True, + environment=request_body.environment, + labels={"owner": credentials.username}, + name=container_name, + restart_policy={"Name": "always"}, + ) + ) + + app.mount( "/", AuthStaticFiles( diff --git a/eslint.config.js b/eslint.config.js index ebd443d..7c1fd93 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -20,6 +20,7 @@ export default [ { rules: { 'sort-imports': 'error', + 'vue/attributes-order': ['error', { alphabetical: true }], 'vue/multi-word-component-names': 'off', }, }, diff --git a/frontend/components/Breadcrumb.vue b/frontend/components/Breadcrumb.vue index 10454ba..126633e 100644 --- a/frontend/components/Breadcrumb.vue +++ b/frontend/components/Breadcrumb.vue @@ -1,5 +1,5 @@