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 os import getenv, path
from typing import Annotated, Any from typing import Annotated
from docker import errors, from_env from docker import errors, from_env
from docker.models.containers import Container from docker.models.containers import Container
@ -7,6 +7,7 @@ from dotenv import load_dotenv
from fastapi import Depends, FastAPI, HTTPException, Request from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from starlette import status, types from starlette import status, types
from uvicorn import run from uvicorn import run
@ -58,24 +59,36 @@ class AuthStaticFiles(StaticFiles):
app = FastAPI(dependencies=[Depends(check_auth)]) app = FastAPI(dependencies=[Depends(check_auth)])
def serialize_container(container: Container) -> dict[str, Any]: class SerializedContainer(BaseModel):
return { id: str
"id": container.short_id, name: str | None
"name": container.name, image: str | None
"image": container.image.tags[0] if container.image else None, labels: dict[str, str]
"labels": container.labels, status: str
"status": container.status, health: str
"health": container.health, ports: dict[str, str]
"ports": container.ports, owner: str | None
"owner": container.labels.get("owner"), environment: list[str]
"environment": container.attrs["Config"]["Env"],
}
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") @app.get("/api/containers")
def get_containers( def get_containers(
credentials: Annotated[HTTPBasicCredentials, Depends(security)], credentials: Annotated[HTTPBasicCredentials, Depends(security)],
) -> list[dict[str, Any]]: ) -> list[SerializedContainer]:
if credentials.username == "admin": if credentials.username == "admin":
return [ return [
serialize_container(container) for container in client.containers.list() serialize_container(container) for container in client.containers.list()
@ -93,7 +106,7 @@ def get_containers(
def get_container( def get_container(
container_name: str, container_name: str,
credentials: Annotated[HTTPBasicCredentials, Depends(security)], credentials: Annotated[HTTPBasicCredentials, Depends(security)],
) -> dict[str, Any]: ) -> SerializedContainer:
try: try:
container = client.containers.get(container_name) container = client.containers.get(container_name)
except errors.APIError: except errors.APIError:
@ -108,6 +121,67 @@ def get_container(
return serialize_container(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( app.mount(
"/", "/",
AuthStaticFiles( AuthStaticFiles(

View File

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

View File

@ -1,5 +1,5 @@
<template> <template>
<nav class="breadcrumb" aria-label="breadcrumbs"> <nav aria-label="breadcrumbs" class="breadcrumb">
<ul> <ul>
<li class="is-active"> <li class="is-active">
<router-link to="/"> <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> <template>
<div :class="loading ? 'skeleton-block' : ''"> <div :class="loading ? 'skeleton-block' : ''">
<slot /> <Message v-if="message" :type="type">{{ message }}</Message>
<slot v-if="!loading && !message" />
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Message from '@/components/Message.vue'
export default { export default {
name: 'Loading', name: 'Loading',
components: {
Message,
},
props: { props: {
loading: { loading: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
message: {
type: String,
default: '',
},
type: {
type: String,
default: 'info',
},
}, },
} }
</script> </script>

View File

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

View File

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

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