diff --git a/Cargo.lock b/Cargo.lock index 2dbb3a8..af3d530 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -266,6 +266,7 @@ dependencies = [ "serde_repr", "serde_urlencoded", "thiserror", + "time", "tokio", "tokio-util", "url", @@ -281,6 +282,7 @@ dependencies = [ "serde", "serde_repr", "serde_with", + "time", ] [[package]] @@ -1083,6 +1085,7 @@ dependencies = [ "serde", "serde_json", "thiserror", + "time", "tokio", "tokio-stream", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 9460c51..de383c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,13 +8,14 @@ anyhow = "1" askama = { version = "0.12", features = ["with-axum"] } askama_axum = "0.3" axum = "0.6" -bollard = "0.15" +bollard = { version = "0.15", features = ["time"] } clap = { version = "4", features = ["env", "derive"] } dotenvy = "0.15" futures-util = "0.3" serde = "1" serde_json = "1" thiserror = "1" +time = { version = "0.3", features = ["serde"] } tokio = { version = "1", features = ["full"] } tokio-stream = "0.1" tracing = "0.1" diff --git a/src/node/container.rs b/src/node/container.rs index 5ccc85e..a202c52 100644 --- a/src/node/container.rs +++ b/src/node/container.rs @@ -1,34 +1,59 @@ use crate::http::error::Result; use bollard::{ container::InspectContainerOptions, - service::{ContainerInspectResponse, ContainerSummary}, + service::{ContainerInspectResponse, ContainerSummary, MountPoint}, Docker, }; use serde::Serialize; +use time::OffsetDateTime; #[derive(Serialize)] pub struct ContainerInfo { + pub id: String, pub name: String, pub state: String, pub image: String, + pub image_id: String, + pub created_at: String, + pub volumes: Option>, + pub env: Option>, } impl From for ContainerInfo { fn from(value: ContainerSummary) -> Self { + let created = OffsetDateTime::from_unix_timestamp(value.created.unwrap()) + .unwrap_or(OffsetDateTime::UNIX_EPOCH); ContainerInfo { - name: value.names.unwrap()[0].trim_start_matches('/').to_string(), - state: value.state.unwrap(), - image: value.image.unwrap(), + id: value.id.unwrap_or_default(), + name: value.names.unwrap_or_default()[0] + .trim_start_matches('/') + .to_string(), + state: value.state.unwrap_or_default(), + image: value.image.unwrap_or_default(), + image_id: value.image_id.unwrap_or_default(), + created_at: created.time().to_string(), + volumes: value.mounts, + env: None, } } } impl From for ContainerInfo { fn from(value: ContainerInspectResponse) -> Self { + let config = value.config.unwrap_or_default(); ContainerInfo { - name: value.name.unwrap(), - state: value.state.and_then(|s| s.status).unwrap().to_string(), - image: value.image.unwrap(), + id: value.id.unwrap_or_default(), + name: value.name.unwrap_or_default(), + state: value + .state + .and_then(|s| s.status) + .unwrap_or_else(|| bollard::service::ContainerStateStatusEnum::EMPTY) + .to_string(), + image: config.image.unwrap_or_default(), + image_id: value.image.unwrap_or_default(), + created_at: value.created.unwrap_or_default(), + volumes: value.mounts, + env: config.env, } } } diff --git a/templates/container/get-one.html b/templates/container/get-one.html index 508581a..79fbbf4 100644 --- a/templates/container/get-one.html +++ b/templates/container/get-one.html @@ -5,11 +5,44 @@ {% block content %}

Container {{container_name}}

-

Status

-
TODO
-

Logs

- - +
+

Status

+
+
ID
+
{{ info.id }}
+
Status
+
{{ info.state }}
+
Image
+
{{ info.image }}
+
Image ID
+
{{ info.image_id }}
+
Created at
+
{{ info.created_at }}
+ {% match info.volumes %} + {% when Some with (volumes) %} +
Volumes
+ {% for volume in volumes %} +
+ {{ volume.source.clone().unwrap_or_default() }} → {{ + volume.destination.clone().unwrap_or_default() }} +
+ {% endfor %} + {% when None %} + {% endmatch %} + {% match info.env %} + {% when Some with (env) %} +
Environment
+ {% for var in env %} +
{{ var }}
+ {% endfor %} + {% when None %} + {% endmatch %} +
+
+
+

Logs

+ +
@@ -98,9 +198,15 @@ } // Received lines of log if ("lines" in data) { - data.lines.split("\n").map(line => convert.toHtml(line)).filter(line => line).forEach(line => { + data.lines.split("\n").forEach(line => { + if (!line) { + return; + } + // Extract timestamp + const firstSpace = line.indexOf(' ') + const [timestamp, logline] = [line.substring(0, firstSpace), line.substring(firstSpace + 1)]; const lineEl = document.createElement("p"); - lineEl.innerHTML = line.trim(); + lineEl.innerHTML = `${convert.toHtml(logline)}`.trim(); logEl.appendChild(lineEl); lineEl.scrollIntoView(); });