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 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(
|
||||||
|
@ -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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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="/">
|
||||||
|
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>
|
<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>
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
<li v-for="container in containers" :key="container.id">
|
<Icon name="plus">New container</Icon>
|
||||||
<router-link :to="`/container/${container.name}`">
|
</router-link>
|
||||||
{{ container.name }}
|
</div>
|
||||||
</router-link>
|
<div class="block">
|
||||||
</li>
|
<ul>
|
||||||
</ul>
|
<li v-for="container in containers" :key="container.id">
|
||||||
|
<router-link :to="`/container/${container.name}`">
|
||||||
|
{{ container.name }}
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</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
0
frontend/views/New.vue
Normal file
Loading…
Reference in New Issue
Block a user