feat: 👔 add working create and update req
This commit is contained in:
parent
cb65233e56
commit
c629b1b682
@ -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(
|
||||
|
@ -20,6 +20,7 @@ export default [
|
||||
{
|
||||
rules: {
|
||||
'sort-imports': 'error',
|
||||
'vue/attributes-order': ['error', { alphabetical: true }],
|
||||
'vue/multi-word-component-names': 'off',
|
||||
},
|
||||
},
|
||||
|
@ -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="/">
|
||||
|
20
frontend/components/Icon.vue
Normal file
20
frontend/components/Icon.vue
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -1,30 +1,36 @@
|
||||
<template>
|
||||
<Loading class="container" :loading="loading">
|
||||
<Message v-if="error" type="danger">{{ error }}</Message>
|
||||
<ul v-if="!loading && !error">
|
||||
<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
0
frontend/views/New.vue
Normal file
Loading…
Reference in New Issue
Block a user