feat: 👔 add working create and update req

This commit is contained in:
Michel Roux 2024-11-03 22:57:12 +01:00
parent cb65233e56
commit c629b1b682
8 changed files with 154 additions and 44 deletions

View File

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

View File

@ -20,6 +20,7 @@ export default [
{
rules: {
'sort-imports': 'error',
'vue/attributes-order': ['error', { alphabetical: true }],
'vue/multi-word-component-names': 'off',
},
},

View File

@ -1,5 +1,5 @@
<template>
<nav class="breadcrumb" aria-label="breadcrumbs">
<nav aria-label="breadcrumbs" class="breadcrumb">
<ul>
<li class="is-active">
<router-link to="/">

View File

@ -0,0 +1,20 @@
<template>
<span class="icon-text">
<span class="icon">
<i :class="`fa fa-${name}`"></i>
</span>
<span><slot /></span>
</span>
</template>
<script lang="ts">
export default {
name: 'Icon',
props: {
name: {
type: String,
required: true,
},
},
}
</script>

View File

@ -1,17 +1,31 @@
<template>
<div :class="loading ? 'skeleton-block' : ''">
<slot />
<Message v-if="message" :type="type">{{ message }}</Message>
<slot v-if="!loading && !message" />
</div>
</template>
<script lang="ts">
import Message from '@/components/Message.vue'
export default {
name: 'Loading',
components: {
Message,
},
props: {
loading: {
type: Boolean,
default: true,
},
message: {
type: String,
default: '',
},
type: {
type: String,
default: 'info',
},
},
}
</script>

View File

@ -1,24 +1,14 @@
<template>
<nav class="navbar" role="navigation" aria-label="main navigation">
<nav aria-label="main navigation" class="navbar" role="navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
<span class="icon-text">
<span class="icon">
<i class="fa fa-paper-plane"></i>
</span>
<span>Pilotwings</span>
</span>
</a>
<router-link class="navbar-item" to="/">
<Icon name="paper-plane">Pilotwings</Icon>
</router-link>
</div>
<div class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item" href="/docs">
<span class="icon-text">
<span class="icon">
<i class="fa fa-book"></i>
</span>
<span>API Documentation</span>
</span>
<Icon name="book">API Documentation</Icon>
</a>
</div>
</div>
@ -26,7 +16,12 @@
</template>
<script lang="ts">
import Icon from './Icon.vue'
export default {
name: 'Navbar',
components: {
Icon,
},
}
</script>

View File

@ -1,30 +1,36 @@
<template>
<Loading class="container" :loading="loading">
<Message v-if="error" type="danger">{{ error }}</Message>
<ul v-if="!loading && !error">
<li v-for="container in containers" :key="container.id">
<router-link :to="`/container/${container.name}`">
{{ container.name }}
</router-link>
</li>
</ul>
<Loading class="container" :loading="loading" :message="error" type="danger">
<div class="block">
<router-link class="button" to="/new">
<Icon name="plus">New container</Icon>
</router-link>
</div>
<div class="block">
<ul>
<li v-for="container in containers" :key="container.id">
<router-link :to="`/container/${container.name}`">
{{ container.name }}
</router-link>
</li>
</ul>
</div>
</Loading>
</template>
<script lang="ts">
import axios, { AxiosError } from 'axios'
import Icon from '@/components/Icon.vue'
import Loading from '@/components/Loading.vue'
import Message from '@/components/Message.vue'
export default {
name: 'Home',
components: {
Icon,
Loading,
Message,
},
data() {
return {
containers: [] as string[],
containers: [],
error: '',
loading: true,
}

0
frontend/views/New.vue Normal file
View File