diff --git a/backend/pilotwings.py b/backend/pilotwings.py index d5f0ecb..807c8d2 100644 --- a/backend/pilotwings.py +++ b/backend/pilotwings.py @@ -5,6 +5,7 @@ from docker import errors, from_env from docker.models.containers import Container from dotenv import load_dotenv from fastapi import Depends, FastAPI, HTTPException, Request +from fastapi.responses import PlainTextResponse from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.staticfiles import StaticFiles from pydantic import BaseModel @@ -55,6 +56,14 @@ class SerializedContainer(BaseModel): engine: str | None owner: str | None environment: list[str] + logs: str | None + + +def container_logs(container: Container, tail: int) -> str | None: + try: + return container.logs(tail=tail).decode() + except errors.APIError: + return None def serialize_container(container: Container) -> SerializedContainer: @@ -68,6 +77,7 @@ def serialize_container(container: Container) -> SerializedContainer: engine=container.labels.get("engine"), owner=container.labels.get("owner"), environment=container.attrs["Config"]["Env"], + logs=container_logs(container, 100), ) @@ -119,7 +129,7 @@ def get_container( class ContainerRequest(BaseModel): image: str - environment: dict[str, str] + environment: list[str] @app.post("/api/container/{container_name}") @@ -133,11 +143,11 @@ def create_or_update_container( if not networks: client.networks.create("pilotwings") + client.images.pull(request_body.image) + try: - container = select_container(container_name, credentials) - container.stop() - container.remove(v=True, force=True) - except errors.APIError: + delete_container(container_name, credentials) + except HTTPException: pass return serialize_container( @@ -153,6 +163,52 @@ def create_or_update_container( ) +@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() + 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) + + class AuthStaticFiles(StaticFiles): async def __call__( self, scope: types.Scope, receive: types.Receive, send: types.Send diff --git a/frontend/App.vue b/frontend/App.vue index fba17c8..79fbbfe 100644 --- a/frontend/App.vue +++ b/frontend/App.vue @@ -2,7 +2,9 @@
- +
+ +
diff --git a/frontend/components/Navbar.vue b/frontend/components/Navbar.vue index bad0393..11a1c80 100644 --- a/frontend/components/Navbar.vue +++ b/frontend/components/Navbar.vue @@ -2,7 +2,7 @@