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 docker.models.containers import Container
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from fastapi import Depends, FastAPI, HTTPException, Request
|
from fastapi import Depends, FastAPI, HTTPException, Request
|
||||||
|
from fastapi.responses import PlainTextResponse
|
||||||
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 pydantic import BaseModel
|
||||||
@ -55,6 +56,14 @@ class SerializedContainer(BaseModel):
|
|||||||
engine: str | None
|
engine: str | None
|
||||||
owner: str | None
|
owner: str | None
|
||||||
environment: list[str]
|
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:
|
def serialize_container(container: Container) -> SerializedContainer:
|
||||||
@ -68,6 +77,7 @@ def serialize_container(container: Container) -> SerializedContainer:
|
|||||||
engine=container.labels.get("engine"),
|
engine=container.labels.get("engine"),
|
||||||
owner=container.labels.get("owner"),
|
owner=container.labels.get("owner"),
|
||||||
environment=container.attrs["Config"]["Env"],
|
environment=container.attrs["Config"]["Env"],
|
||||||
|
logs=container_logs(container, 100),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -119,7 +129,7 @@ def get_container(
|
|||||||
|
|
||||||
class ContainerRequest(BaseModel):
|
class ContainerRequest(BaseModel):
|
||||||
image: str
|
image: str
|
||||||
environment: dict[str, str]
|
environment: list[str]
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/container/{container_name}")
|
@app.post("/api/container/{container_name}")
|
||||||
@ -133,11 +143,11 @@ def create_or_update_container(
|
|||||||
if not networks:
|
if not networks:
|
||||||
client.networks.create("pilotwings")
|
client.networks.create("pilotwings")
|
||||||
|
|
||||||
|
client.images.pull(request_body.image)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
container = select_container(container_name, credentials)
|
delete_container(container_name, credentials)
|
||||||
container.stop()
|
except HTTPException:
|
||||||
container.remove(v=True, force=True)
|
|
||||||
except errors.APIError:
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return serialize_container(
|
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):
|
class AuthStaticFiles(StaticFiles):
|
||||||
async def __call__(
|
async def __call__(
|
||||||
self, scope: types.Scope, receive: types.Receive, send: types.Send
|
self, scope: types.Scope, receive: types.Receive, send: types.Send
|
||||||
|
@ -2,7 +2,9 @@
|
|||||||
<Navbar />
|
<Navbar />
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<Breadcrumb />
|
<Breadcrumb />
|
||||||
<RouterView />
|
<div class="container">
|
||||||
|
<RouterView />
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<nav aria-label="main navigation" class="navbar" role="navigation">
|
<nav aria-label="main navigation" class="navbar" role="navigation">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<router-link class="navbar-item" to="/">
|
<router-link class="navbar-item" to="/">
|
||||||
<Icon name="paper-plane">Pilotwings</Icon>
|
<Icon name="send">Pilotwings</Icon>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-menu">
|
<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 Container from '../views/Container.vue'
|
||||||
import Home from '../views/Home.vue'
|
import Home from '../views/Home.vue'
|
||||||
|
import New from '../views/New.vue'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
component: Home,
|
component: Home,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/new',
|
||||||
|
component: New,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/container/:name',
|
path: '/container/:name',
|
||||||
component: Container,
|
component: Container,
|
||||||
|
@ -5,6 +5,8 @@ export interface Container {
|
|||||||
labels: Record<string, string>
|
labels: Record<string, string>
|
||||||
status: string
|
status: string
|
||||||
health: string
|
health: string
|
||||||
owner: string
|
engine: string | null
|
||||||
|
owner: string | null
|
||||||
environment: string[]
|
environment: string[]
|
||||||
|
logs: string | null
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,168 @@
|
|||||||
<template>
|
<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>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<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 {
|
export default {
|
||||||
name: 'Container',
|
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>
|
</script>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<Loading class="container" :loading="loading" :message="error" type="danger">
|
<Loading class="content" :loading="loading" :message="error" type="danger">
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<router-link class="button" to="/new">
|
<router-link class="button" to="/new">
|
||||||
<Icon name="plus">New container</Icon>
|
<Icon name="plus">New container</Icon>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
<div v-if="containers.length" class="box">
|
||||||
<ul>
|
<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}`">
|
||||||
|
@ -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