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 @@