feat: ✨ all functionnalities on status page work
This commit is contained in:
parent
f6ba1ac476
commit
5693656cc2
@ -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
|
||||
|
@ -2,7 +2,9 @@
|
||||
<Navbar />
|
||||
<section class="section">
|
||||
<Breadcrumb />
|
||||
<RouterView />
|
||||
<div class="container">
|
||||
<RouterView />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
<nav aria-label="main navigation" class="navbar" role="navigation">
|
||||
<div class="navbar-brand">
|
||||
<router-link class="navbar-item" to="/">
|
||||
<Icon name="paper-plane">Pilotwings</Icon>
|
||||
<Icon name="send">Pilotwings</Icon>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="navbar-menu">
|
||||
|
@ -1,14 +1,19 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import Container from '../views/Container.vue'
|
||||
import Home from '../views/Home.vue'
|
||||
import New from '../views/New.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
component: Home,
|
||||
},
|
||||
{
|
||||
path: '/new',
|
||||
component: New,
|
||||
},
|
||||
{
|
||||
path: '/container/:name',
|
||||
component: Container,
|
||||
|
@ -5,6 +5,8 @@ export interface Container {
|
||||
labels: Record<string, string>
|
||||
status: string
|
||||
health: string
|
||||
owner: string
|
||||
engine: string | null
|
||||
owner: string | null
|
||||
environment: string[]
|
||||
logs: string | null
|
||||
}
|
||||
|
@ -1,9 +1,168 @@
|
||||
<template>
|
||||
<oui>oui</oui>
|
||||
<Loading class="content" :loading="loading" :message="error" type="danger">
|
||||
<div class="block">
|
||||
<h1>
|
||||
<Icon name="cube">{{ container_name }}</Icon>
|
||||
<router-link class="button ml-2" :to="`/update/${container_name}`">
|
||||
<Icon name="edit">Update</Icon>
|
||||
</router-link>
|
||||
<button class="button ml-2" @click="refresh">
|
||||
<Icon name="refresh">Refresh</Icon>
|
||||
</button>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="block is-flex is-align-items-baseline">
|
||||
<Icon name="image">
|
||||
<b>Image:</b>
|
||||
<span class="m-2">{{ container?.image }}</span>
|
||||
</Icon>
|
||||
<button class="button ml-2" @click="pull">
|
||||
<Icon name="cloud-download">Pull</Icon>
|
||||
</button>
|
||||
<button class="button ml-2" @click="remove">
|
||||
<Icon name="trash">Remove</Icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="block is-flex is-align-items-baseline">
|
||||
<Icon name="support">
|
||||
<b>Status:</b>
|
||||
</Icon>
|
||||
<Icon
|
||||
:class="`has-text-${status_icon.type} m-2`"
|
||||
:name="status_icon.name"
|
||||
>
|
||||
{{ container?.status }}
|
||||
</Icon>
|
||||
<button class="button ml-2" @click="restart">
|
||||
<Icon name="refresh">Restart</Icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="block is-flex is-align-items-baseline">
|
||||
<Icon name="image"><b>Logs:</b></Icon>
|
||||
<a
|
||||
class="button ml-2"
|
||||
:download="`${container_name}_logs.txt`"
|
||||
:href="`/api/container/${container_name}/logs`"
|
||||
>
|
||||
<Icon name="download">Download</Icon>
|
||||
</a>
|
||||
</div>
|
||||
<pre>{{ container?.logs?.trim().split('\n').reverse().join('\n') }}</pre>
|
||||
</div>
|
||||
</Loading>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import axios, { AxiosError } from 'axios'
|
||||
import type { Container } from '../types'
|
||||
import Icon from '../components/Icon.vue'
|
||||
import Loading from '../components/Loading.vue'
|
||||
|
||||
export default {
|
||||
name: 'Container',
|
||||
components: {
|
||||
Icon,
|
||||
Loading,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
container: null as Container | null,
|
||||
error: '',
|
||||
loading: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
container_name(): string {
|
||||
return this.$route.params.name as string
|
||||
},
|
||||
status_icon(): { name: string; type: string } {
|
||||
if (!this.container) return { name: 'question-circle', type: 'warning' }
|
||||
switch (this.container.status) {
|
||||
case 'running':
|
||||
return { name: 'play', type: 'success' }
|
||||
case 'restarting':
|
||||
return { name: 'refresh', type: 'danger' }
|
||||
case 'paused':
|
||||
return { name: 'pause', type: 'warning' }
|
||||
case 'exited':
|
||||
return { name: 'stop', type: 'danger' }
|
||||
default:
|
||||
return { name: 'question-circle', type: 'warning' }
|
||||
}
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await this.refresh()
|
||||
},
|
||||
methods: {
|
||||
async pull() {
|
||||
try {
|
||||
this.loading = true
|
||||
const response = await axios.post<Container>(
|
||||
`/api/container/${this.container_name}/pull`,
|
||||
)
|
||||
this.container = response.data
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
this.error =
|
||||
error instanceof AxiosError
|
||||
? error.message
|
||||
: 'Error pulling container'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async refresh() {
|
||||
try {
|
||||
this.loading = true
|
||||
const response = await axios.get<Container>(
|
||||
`/api/container/${this.container_name}`,
|
||||
)
|
||||
this.container = response.data
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
this.error =
|
||||
error instanceof AxiosError
|
||||
? error.message
|
||||
: 'Error loading container'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async remove() {
|
||||
if (!confirm('Are you sure you want to remove this container?')) return
|
||||
try {
|
||||
this.loading = true
|
||||
await axios.delete(`/api/container/${this.container_name}`)
|
||||
this.$router.push('/')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
this.error =
|
||||
error instanceof AxiosError
|
||||
? error.message
|
||||
: 'Error deleting container'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async restart() {
|
||||
try {
|
||||
this.loading = true
|
||||
const response = await axios.post<Container>(
|
||||
`/api/container/${this.container_name}/restart`,
|
||||
)
|
||||
this.container = response.data
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
this.error =
|
||||
error instanceof AxiosError
|
||||
? error.message
|
||||
: 'Error restarting container'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<Loading class="container" :loading="loading" :message="error" type="danger">
|
||||
<Loading class="content" :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">
|
||||
<div v-if="containers.length" class="box">
|
||||
<ul>
|
||||
<li v-for="container in containers" :key="container.id">
|
||||
<router-link :to="`/container/${container.name}`">
|
||||
|
@ -0,0 +1,7 @@
|
||||
<template><oui>oui</oui></template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'New',
|
||||
}
|
||||
</script>
|
Loading…
Reference in New Issue
Block a user