wouldnt it be funny if I didn't commit for 24 hours straight
This commit is contained in:
parent
6e30007b4a
commit
a87d1df9ef
15 changed files with 782 additions and 112 deletions
109
Cargo.lock
generated
109
Cargo.lock
generated
|
@ -377,6 +377,39 @@ version = "0.8.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-deque"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"crossbeam-epoch",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-epoch"
|
||||||
|
version = "0.9.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"cfg-if",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"memoffset",
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
@ -393,6 +426,12 @@ version = "0.15.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
@ -717,6 +756,15 @@ version = "2.6.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
|
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memoffset"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mime"
|
name = "mime"
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
|
@ -769,6 +817,15 @@ dependencies = [
|
||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ntapi"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.46.0"
|
version = "0.46.0"
|
||||||
|
@ -904,6 +961,26 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"rayon-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon-core"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-deque",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
@ -1084,12 +1161,14 @@ dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sysinfo",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"xshell",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1115,6 +1194,21 @@ version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
|
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sysinfo"
|
||||||
|
version = "0.29.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0a18d114d420ada3a891e6bc8e96a2023402203296a47cdd65083377dad18ba5"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
"ntapi",
|
||||||
|
"once_cell",
|
||||||
|
"rayon",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.50"
|
version = "1.0.50"
|
||||||
|
@ -1560,3 +1654,18 @@ name = "windows_x86_64_msvc"
|
||||||
version = "0.48.5"
|
version = "0.48.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xshell"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce2107fe03e558353b4c71ad7626d58ed82efaf56c54134228608893c77023ad"
|
||||||
|
dependencies = [
|
||||||
|
"xshell-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xshell-macros"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e2c411759b501fb9501aac2b1b2d287a6e93e5bdcf13c25306b23e1b716dd0e"
|
||||||
|
|
|
@ -14,9 +14,11 @@ dotenvy = "0.15"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
serde = "1"
|
serde = "1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
sysinfo = "0.29.10"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
time = { version = "0.3", features = ["serde"] }
|
time = { version = "0.3", features = ["serde"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
|
xshell = "0.2"
|
||||||
|
|
|
@ -22,6 +22,9 @@ pub enum AppError {
|
||||||
#[error("file error: {0}")]
|
#[error("file error: {0}")]
|
||||||
FileError(#[from] std::io::Error),
|
FileError(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error("shell error: {0}")]
|
||||||
|
ShellError(#[from] xshell::Error),
|
||||||
|
|
||||||
#[error("unexpected internal error: {0}")]
|
#[error("unexpected internal error: {0}")]
|
||||||
Internal(#[from] anyhow::Error),
|
Internal(#[from] anyhow::Error),
|
||||||
|
|
||||||
|
@ -51,6 +54,11 @@ impl IntoResponse for AppError {
|
||||||
code: "file-error".to_string(),
|
code: "file-error".to_string(),
|
||||||
message: err.to_string(),
|
message: err.to_string(),
|
||||||
},
|
},
|
||||||
|
AppError::ShellError(err) => ErrorInfo {
|
||||||
|
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
code: "shell-error".to_string(),
|
||||||
|
message: err.to_string(),
|
||||||
|
},
|
||||||
AppError::Template(err) => ErrorInfo {
|
AppError::Template(err) => ErrorInfo {
|
||||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
code: "template-error".to_string(),
|
code: "template-error".to_string(),
|
||||||
|
|
10
src/main.rs
10
src/main.rs
|
@ -21,11 +21,20 @@ struct Args {
|
||||||
/// Address:port to bind
|
/// Address:port to bind
|
||||||
#[arg(short, long, default_value = "0.0.0.0:3000", env = "STAX_BIND")]
|
#[arg(short, long, default_value = "0.0.0.0:3000", env = "STAX_BIND")]
|
||||||
bind: SocketAddr,
|
bind: SocketAddr,
|
||||||
|
|
||||||
|
/// Path to arion
|
||||||
|
#[arg(
|
||||||
|
long = "arion-bin",
|
||||||
|
env = "STAX_ARION_BIN",
|
||||||
|
default_value = "/run/current-system/sw/bin/arion"
|
||||||
|
)]
|
||||||
|
arion_binary: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub stack_dir: PathBuf,
|
pub stack_dir: PathBuf,
|
||||||
|
pub arion_bin: PathBuf,
|
||||||
pub docker: Docker,
|
pub docker: Docker,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,6 +56,7 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
stack_dir: args.stack_dir,
|
stack_dir: args.stack_dir,
|
||||||
|
arion_bin: args.arion_binary,
|
||||||
docker,
|
docker,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::http::error::Result;
|
use crate::http::error::Result;
|
||||||
use bollard::{
|
use bollard::{
|
||||||
container::InspectContainerOptions,
|
container::{
|
||||||
|
InspectContainerOptions, KillContainerOptions, RemoveContainerOptions,
|
||||||
|
RestartContainerOptions, StartContainerOptions, StopContainerOptions,
|
||||||
|
},
|
||||||
service::{ContainerInspectResponse, ContainerSummary, MountPoint},
|
service::{ContainerInspectResponse, ContainerSummary, MountPoint},
|
||||||
Docker,
|
Docker,
|
||||||
};
|
};
|
||||||
|
@ -17,6 +22,15 @@ pub struct ContainerInfo {
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
pub volumes: Option<Vec<MountPoint>>,
|
pub volumes: Option<Vec<MountPoint>>,
|
||||||
pub env: Option<Vec<String>>,
|
pub env: Option<Vec<String>>,
|
||||||
|
pub labels: Option<HashMap<String, String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContainerInfo {
|
||||||
|
pub fn stack(&self) -> Option<String> {
|
||||||
|
self.labels
|
||||||
|
.clone()
|
||||||
|
.and_then(|lab| lab.get("com.docker.compose.project").cloned())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ContainerSummary> for ContainerInfo {
|
impl From<ContainerSummary> for ContainerInfo {
|
||||||
|
@ -33,6 +47,7 @@ impl From<ContainerSummary> for ContainerInfo {
|
||||||
image_id: value.image_id.unwrap_or_default(),
|
image_id: value.image_id.unwrap_or_default(),
|
||||||
created_at: created.time().to_string(),
|
created_at: created.time().to_string(),
|
||||||
volumes: value.mounts,
|
volumes: value.mounts,
|
||||||
|
labels: value.labels,
|
||||||
env: None,
|
env: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,6 +68,7 @@ impl From<ContainerInspectResponse> for ContainerInfo {
|
||||||
image_id: value.image.unwrap_or_default(),
|
image_id: value.image.unwrap_or_default(),
|
||||||
created_at: value.created.unwrap_or_default(),
|
created_at: value.created.unwrap_or_default(),
|
||||||
volumes: value.mounts,
|
volumes: value.mounts,
|
||||||
|
labels: config.labels,
|
||||||
env: config.env,
|
env: config.env,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,3 +81,40 @@ pub async fn get_info(docker: &Docker, name: &str) -> Result<ContainerInfo> {
|
||||||
|
|
||||||
Ok(info.into())
|
Ok(info.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn start(docker: &Docker, name: &str) -> Result<()> {
|
||||||
|
Ok(docker
|
||||||
|
.start_container(&name, None::<StartContainerOptions<String>>)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn restart(docker: &Docker, name: &str) -> Result<()> {
|
||||||
|
Ok(docker
|
||||||
|
.restart_container(&name, Some(RestartContainerOptions { t: 30 }))
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn stop(docker: &Docker, name: &str) -> Result<()> {
|
||||||
|
Ok(docker
|
||||||
|
.stop_container(&name, Some(StopContainerOptions { t: 30 }))
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn kill(docker: &Docker, name: &str) -> Result<()> {
|
||||||
|
Ok(docker
|
||||||
|
.kill_container(&name, None::<KillContainerOptions<String>>)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove(docker: &Docker, name: &str) -> Result<()> {
|
||||||
|
Ok(docker
|
||||||
|
.remove_container(
|
||||||
|
&name,
|
||||||
|
Some(RemoveContainerOptions {
|
||||||
|
v: false,
|
||||||
|
force: true,
|
||||||
|
link: false,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
pub mod container;
|
pub mod container;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod stack;
|
pub mod stack;
|
||||||
|
pub mod system;
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
use super::error::StackError;
|
use super::{container::ContainerInfo, error::StackError};
|
||||||
use crate::http::error::Result;
|
use crate::http::error::Result;
|
||||||
use bollard::{container::ListContainersOptions, service::ContainerSummary, Docker};
|
use bollard::{container::ListContainersOptions, service::ContainerSummary, Docker};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::{collections::HashMap, path::Path};
|
use std::{collections::HashMap, path::Path};
|
||||||
use tokio::fs::{read_dir, try_exists};
|
use tokio::fs::{read_dir, try_exists};
|
||||||
|
use xshell::Shell;
|
||||||
|
|
||||||
const COMPOSE_FILE: &str = "arion-compose.nix";
|
const COMPOSE_FILE: &str = "arion-compose.nix";
|
||||||
|
|
||||||
|
@ -41,18 +42,26 @@ pub struct StackInfo {
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list(base_dir: &Path, docker: &Docker) -> Result<Vec<StackInfo>> {
|
#[derive(Serialize)]
|
||||||
let containers = docker
|
pub struct NodeInfo {
|
||||||
.list_containers(Some(ListContainersOptions {
|
pub stacks: Vec<StackInfo>,
|
||||||
all: false,
|
pub containers: Vec<ContainerInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list(base_dir: &Path, docker: &Docker) -> Result<NodeInfo> {
|
||||||
|
let containers: Vec<ContainerInfo> = docker
|
||||||
|
.list_containers(Some(ListContainersOptions::<String> {
|
||||||
|
all: true,
|
||||||
limit: None,
|
limit: None,
|
||||||
filters: HashMap::from([("status".to_string(), vec!["running".to_string()])]),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}))
|
}))
|
||||||
.await?;
|
.await?
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.clone().into())
|
||||||
|
.collect();
|
||||||
|
|
||||||
let mut dirs = read_dir(base_dir).await?;
|
let mut dirs = read_dir(base_dir).await?;
|
||||||
let mut stacklist = vec![];
|
let mut stacks = vec![];
|
||||||
while let Some(dir) = dirs.next_entry().await? {
|
while let Some(dir) = dirs.next_entry().await? {
|
||||||
let meta = dir.metadata().await?;
|
let meta = dir.metadata().await?;
|
||||||
if !meta.is_dir() {
|
if !meta.is_dir() {
|
||||||
|
@ -61,17 +70,48 @@ pub async fn list(base_dir: &Path, docker: &Docker) -> Result<Vec<StackInfo>> {
|
||||||
if is_stack(&dir.path()).await? {
|
if is_stack(&dir.path()).await? {
|
||||||
let name = dir.file_name().to_string_lossy().to_string();
|
let name = dir.file_name().to_string_lossy().to_string();
|
||||||
// Check status by analyzing containers
|
// Check status by analyzing containers
|
||||||
let active = containers.iter().any(|cont| {
|
let active = containers
|
||||||
let project = cont
|
.iter()
|
||||||
.clone()
|
.any(|cont| cont.state == "running" && cont.stack() == Some(name.clone()));
|
||||||
.labels
|
stacks.push(StackInfo { name, active })
|
||||||
.and_then(|lab| lab.get("com.docker.compose.project").cloned())
|
|
||||||
.unwrap_or_default();
|
|
||||||
name == project
|
|
||||||
});
|
|
||||||
stacklist.push(StackInfo { name, active })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(stacklist)
|
Ok(NodeInfo { stacks, containers })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn command(
|
||||||
|
base_dir: &Path,
|
||||||
|
stack_name: &str,
|
||||||
|
arion_bin: &Path,
|
||||||
|
action: StackCommand,
|
||||||
|
) -> Result<()> {
|
||||||
|
let dir = base_dir.join(stack_name);
|
||||||
|
if !is_stack(&dir).await? {
|
||||||
|
return Err(StackError::NotFound.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let sh = Shell::new()?;
|
||||||
|
sh.change_dir(dir);
|
||||||
|
sh.cmd(arion_bin).args(action.command()).run()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum StackCommand {
|
||||||
|
Down,
|
||||||
|
Start,
|
||||||
|
Stop,
|
||||||
|
Restart,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StackCommand {
|
||||||
|
fn command(&self) -> &[&str] {
|
||||||
|
match self {
|
||||||
|
StackCommand::Down => &["down"],
|
||||||
|
StackCommand::Start => &["up", "-d"],
|
||||||
|
StackCommand::Stop => &["stop"],
|
||||||
|
StackCommand::Restart => &["restart"],
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
89
src/node/system.rs
Normal file
89
src/node/system.rs
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
use serde::Serialize;
|
||||||
|
use sysinfo::{CpuExt, DiskExt, NetworkExt, NetworksExt, RefreshKind, System, SystemExt};
|
||||||
|
use tokio::sync::OnceCell;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct NetworkInterface {
|
||||||
|
pub name: String,
|
||||||
|
pub received: u64,
|
||||||
|
pub transmitted: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct CpuInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub usage: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct DiskInfo {
|
||||||
|
pub device_name: String,
|
||||||
|
pub mount_point: String,
|
||||||
|
pub total_space: u64,
|
||||||
|
pub available_space: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct SystemInfo {
|
||||||
|
// Node info
|
||||||
|
pub name: String,
|
||||||
|
pub host_name: String,
|
||||||
|
pub os_version: String,
|
||||||
|
pub kernel_version: String,
|
||||||
|
|
||||||
|
// RAM usage
|
||||||
|
pub total_memory: u64,
|
||||||
|
pub used_memory: u64,
|
||||||
|
pub total_swap: u64,
|
||||||
|
pub used_swap: u64,
|
||||||
|
|
||||||
|
// Multi-elem resources
|
||||||
|
pub networks: Vec<NetworkInterface>,
|
||||||
|
pub disks: Vec<DiskInfo>,
|
||||||
|
|
||||||
|
// CPU
|
||||||
|
pub load_average: (f64, f64, f64),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn system_info() -> SystemInfo {
|
||||||
|
let refresh_kind: RefreshKind = RefreshKind::new().with_disks_list().with_memory();
|
||||||
|
let mut sys = System::new_with_specifics(refresh_kind);
|
||||||
|
sys.refresh_specifics(refresh_kind);
|
||||||
|
|
||||||
|
let networks = sys
|
||||||
|
.networks()
|
||||||
|
.iter()
|
||||||
|
.map(|(name, data)| NetworkInterface {
|
||||||
|
name: name.clone(),
|
||||||
|
received: data.received(),
|
||||||
|
transmitted: data.transmitted(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let avg = sys.load_average();
|
||||||
|
|
||||||
|
let disks = sys
|
||||||
|
.disks()
|
||||||
|
.iter()
|
||||||
|
.map(|disk| DiskInfo {
|
||||||
|
device_name: disk.name().to_str().unwrap_or_default().to_string(),
|
||||||
|
mount_point: disk.mount_point().to_str().unwrap_or_default().to_string(),
|
||||||
|
total_space: disk.total_space(),
|
||||||
|
available_space: disk.available_space(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
SystemInfo {
|
||||||
|
name: sys.name().unwrap_or_default(),
|
||||||
|
host_name: sys.host_name().unwrap_or_default(),
|
||||||
|
os_version: sys.os_version().unwrap_or_default(),
|
||||||
|
kernel_version: sys.kernel_version().unwrap_or_default(),
|
||||||
|
total_memory: sys.total_memory(),
|
||||||
|
used_memory: sys.used_memory(),
|
||||||
|
total_swap: sys.total_swap(),
|
||||||
|
used_swap: sys.used_swap(),
|
||||||
|
networks,
|
||||||
|
disks,
|
||||||
|
load_average: (avg.one, avg.five, avg.fifteen),
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,8 @@ use crate::{
|
||||||
accept::ExtractAccept,
|
accept::ExtractAccept,
|
||||||
response::{reply, HandlerResponse},
|
response::{reply, HandlerResponse},
|
||||||
},
|
},
|
||||||
node::container::{get_info, ContainerInfo},
|
node::container::{get_info, kill, remove, restart, start, stop, ContainerInfo},
|
||||||
|
route::AppError,
|
||||||
AppState,
|
AppState,
|
||||||
};
|
};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
@ -14,9 +15,9 @@ use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::{
|
response::{
|
||||||
sse::{Event, KeepAlive},
|
sse::{Event, KeepAlive},
|
||||||
Sse,
|
Redirect, Sse,
|
||||||
},
|
},
|
||||||
routing::get,
|
routing::{get, post},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use bollard::container::LogsOptions;
|
use bollard::container::LogsOptions;
|
||||||
|
@ -108,8 +109,36 @@ async fn get_log_stream(container_name: String, state: AppState) -> impl IntoRes
|
||||||
Sse::new(stream).keep_alive(KeepAlive::default())
|
Sse::new(stream).keep_alive(KeepAlive::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! container_command {
|
||||||
|
($cmd: ident) => {
|
||||||
|
move |Path(cont_name): Path<String>, State(state): State<AppState>| async move {
|
||||||
|
$cmd(&state.docker, &cont_name).await?;
|
||||||
|
Ok(Redirect::to("./")) as Result<Redirect, AppError>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn router() -> Router<AppState> {
|
pub(super) fn router() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/:container", get(get_one))
|
.route(
|
||||||
|
"/:container",
|
||||||
|
get(|Path(cont_name): Path<String>| async move {
|
||||||
|
Redirect::permanent(format!("{}/", &cont_name).as_str())
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route("/:container/", get(get_one))
|
||||||
.route("/:container/log", get(get_log))
|
.route("/:container/log", get(get_log))
|
||||||
|
.route("/:container/start", post(container_command!(start)))
|
||||||
|
.route("/:container/stop", post(container_command!(stop)))
|
||||||
|
.route("/:container/restart", post(container_command!(restart)))
|
||||||
|
.route("/:container/kill", post(container_command!(kill)))
|
||||||
|
.route(
|
||||||
|
"/:container/remove",
|
||||||
|
post(
|
||||||
|
move |Path(cont_name): Path<String>, State(state): State<AppState>| async move {
|
||||||
|
remove(&state.docker, &cont_name).await?;
|
||||||
|
Ok(Redirect::to("/")) as Result<Redirect, AppError>
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,14 @@ use crate::{
|
||||||
error::AppError,
|
error::AppError,
|
||||||
response::{reply, HandlerResponse},
|
response::{reply, HandlerResponse},
|
||||||
},
|
},
|
||||||
node::stack::{list, StackInfo},
|
node::{
|
||||||
|
stack::{list, NodeInfo},
|
||||||
|
system::{system_info, SystemInfo},
|
||||||
|
},
|
||||||
AppState,
|
AppState,
|
||||||
};
|
};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::{extract::State, routing::get, Router};
|
use axum::{extract::State, response::IntoResponse, routing::get, Json, Router};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
mod container;
|
mod container;
|
||||||
|
@ -16,19 +19,29 @@ mod stack;
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "home.html")]
|
#[template(path = "home.html")]
|
||||||
struct HomeTemplate {
|
struct HomeTemplate {
|
||||||
stacks: Vec<StackInfo>,
|
info: NodeInfo,
|
||||||
|
system: SystemInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn home(State(state): State<AppState>) -> HandlerResponse {
|
async fn home(State(state): State<AppState>) -> HandlerResponse {
|
||||||
let list = list(&state.stack_dir, &state.docker)
|
let info = list(&state.stack_dir, &state.docker)
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
reply(json!({ "stacks": list }), HomeTemplate { stacks: list })
|
let system = system_info();
|
||||||
|
reply(
|
||||||
|
json!({ "info": info, "system": system }),
|
||||||
|
HomeTemplate { info, system },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_sys_info() -> impl IntoResponse {
|
||||||
|
Json(system_info())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn router() -> Router<AppState> {
|
pub(crate) fn router() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(home))
|
.route("/", get(home))
|
||||||
|
.route("/sysinfo", get(get_sys_info))
|
||||||
.nest("/stack", stack::router())
|
.nest("/stack", stack::router())
|
||||||
.nest("/container", container::router())
|
.nest("/container", container::router())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
routing::get,
|
response::Redirect,
|
||||||
|
routing::{get, post},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
|
use futures_util::join;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
http::response::{reply, HandlerResponse},
|
http::{
|
||||||
|
error::AppError,
|
||||||
|
response::{reply, HandlerResponse},
|
||||||
|
},
|
||||||
node::{
|
node::{
|
||||||
container::ContainerInfo,
|
container::ContainerInfo,
|
||||||
stack::{get_compose, get_containers},
|
stack::{command, get_compose, get_containers, StackCommand},
|
||||||
},
|
},
|
||||||
AppState,
|
AppState,
|
||||||
};
|
};
|
||||||
|
@ -24,8 +29,13 @@ struct GetOneTemplate {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_one(Path(stack_name): Path<String>, State(state): State<AppState>) -> HandlerResponse {
|
async fn get_one(Path(stack_name): Path<String>, State(state): State<AppState>) -> HandlerResponse {
|
||||||
let file_contents = get_compose(&state.stack_dir, &stack_name).await?;
|
let (file_contents_res, containers_res) = join!(
|
||||||
let containers = get_containers(&state.docker, &stack_name).await?;
|
get_compose(&state.stack_dir, &stack_name),
|
||||||
|
get_containers(&state.docker, &stack_name)
|
||||||
|
);
|
||||||
|
|
||||||
|
let file_contents = file_contents_res?;
|
||||||
|
let containers = containers_res?;
|
||||||
|
|
||||||
reply(
|
reply(
|
||||||
json!({
|
json!({
|
||||||
|
@ -41,6 +51,29 @@ async fn get_one(Path(stack_name): Path<String>, State(state): State<AppState>)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn router() -> Router<AppState> {
|
macro_rules! stack_command {
|
||||||
Router::new().route("/:stack", get(get_one))
|
($cmd: expr) => {
|
||||||
|
move |Path(stack_name): Path<String>, State(state): State<AppState>| async move {
|
||||||
|
command(&state.stack_dir, &stack_name, &state.arion_bin, $cmd).await?;
|
||||||
|
Ok(Redirect::to("./")) as Result<Redirect, AppError>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn router() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route(
|
||||||
|
"/:stack",
|
||||||
|
get(|Path(stack_name): Path<String>| async move {
|
||||||
|
Redirect::permanent(format!("{}/", &stack_name).as_str())
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route("/:stack/", get(get_one))
|
||||||
|
.route("/:stack/start", post(stack_command!(StackCommand::Start)))
|
||||||
|
.route(
|
||||||
|
"/:stack/restart",
|
||||||
|
post(stack_command!(StackCommand::Restart)),
|
||||||
|
)
|
||||||
|
.route("/:stack/stop", post(stack_command!(StackCommand::Stop)))
|
||||||
|
.route("/:stack/down", post(stack_command!(StackCommand::Down)))
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,20 @@
|
||||||
<link rel="stylesheet" href="https://iosevka-webfonts.github.io/iosevka/iosevka.css">
|
<link rel="stylesheet" href="https://iosevka-webfonts.github.io/iosevka/iosevka.css">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
background-color: #13131E;
|
--background: #13131E;
|
||||||
color: #E0DFFE;
|
--bg-raised: #171625;
|
||||||
|
--text: #E0DFFE;
|
||||||
|
|
||||||
|
--link: #FFC53D;
|
||||||
|
--link-hover: #e28d0e;
|
||||||
|
|
||||||
|
--button-color: #B1A9FF;
|
||||||
|
--button-bg: #202248;
|
||||||
|
--button-border: #5B5BD6;
|
||||||
|
--button-bg-hover: #7E451D;
|
||||||
|
|
||||||
|
background-color: var(--background);
|
||||||
|
color: var(--text);
|
||||||
font-family: Inter, sans-serif;
|
font-family: Inter, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,10 +34,10 @@
|
||||||
|
|
||||||
a[href],
|
a[href],
|
||||||
a[href]:visited {
|
a[href]:visited {
|
||||||
color: #FFC53D;
|
color: var(--link);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #e28d0e;
|
color: var(--link-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +49,7 @@
|
||||||
nav {
|
nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #171625;
|
background-color: var(--bg-raised);
|
||||||
|
|
||||||
& a {
|
& a {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -46,18 +58,18 @@
|
||||||
padding: 5px 8px;
|
padding: 5px 8px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
box-shadow: inset 0 -2px #202248;
|
box-shadow: inset 0 -2px var(--button-bg);
|
||||||
font-size: 11pt;
|
font-size: 11pt;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|
||||||
&[href],
|
&[href],
|
||||||
&[href]:visited {
|
&[href]:visited {
|
||||||
color: #B1A9FF;
|
color: var(--button-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #1E160F;
|
background-color: #1E160F;
|
||||||
box-shadow: inset 0 -2px #7E451D;
|
box-shadow: inset 0 -2px var(--button-bg-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,6 +84,21 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: var(--button-bg);
|
||||||
|
color: var(--button-color);
|
||||||
|
border: 1px solid var(--button-border);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--button-bg-hover);
|
||||||
|
color: var(--link-hover);
|
||||||
|
border-color: var(--link-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,22 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main>
|
<main>
|
||||||
<h1>Container <span class="container-name">{{container_name}}</span></h1>
|
<header>
|
||||||
|
<h1>Container <span class="container-name">{{container_name}}</span> {% match info.stack() %}{% when
|
||||||
|
Some
|
||||||
|
with (stack) %}(<a class="stack-name" href="/stack/{{stack}}/">{{stack}}</a>)
|
||||||
|
{% when None %}{% endmatch %}</h1>
|
||||||
|
<div class="actions">
|
||||||
|
<form action="./start" method="POST"><button type="submit">Start</button></form>
|
||||||
|
<form action="./restart" method="POST"><button type="submit">Restart</button></form>
|
||||||
|
<form action="./stop" method="POST"><button type="submit">Stop</button></form>
|
||||||
|
<form action="./kill" method="POST"><button type="submit">Kill</button></form>
|
||||||
|
<form action="./remove" method="POST"><button type="submit">Delete</button></form>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
<section class="status-section">
|
<section class="status-section">
|
||||||
<h2>Status</h2>
|
<h2>Status</h2>
|
||||||
<dl>
|
<dl class="table">
|
||||||
<dt>ID</dt>
|
<dt>ID</dt>
|
||||||
<dd>{{ info.id }}</dd>
|
<dd>{{ info.id }}</dd>
|
||||||
<dt>Status</dt>
|
<dt>Status</dt>
|
||||||
|
@ -18,6 +30,8 @@
|
||||||
<dd>{{ info.image_id }}</dd>
|
<dd>{{ info.image_id }}</dd>
|
||||||
<dt>Created at</dt>
|
<dt>Created at</dt>
|
||||||
<dd>{{ info.created_at }}</dd>
|
<dd>{{ info.created_at }}</dd>
|
||||||
|
</dl>
|
||||||
|
<dl class="list">
|
||||||
{% match info.volumes %}
|
{% match info.volumes %}
|
||||||
{% when Some with (volumes) %}
|
{% when Some with (volumes) %}
|
||||||
<dt>Volumes</dt>
|
<dt>Volumes</dt>
|
||||||
|
@ -37,6 +51,15 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% when None %}
|
{% when None %}
|
||||||
{% endmatch %}
|
{% endmatch %}
|
||||||
|
{% match info.labels %}
|
||||||
|
{% when Some with (labels) %}
|
||||||
|
<dt>Labels</dt>
|
||||||
|
{% for label in labels %}
|
||||||
|
<dd class="label"><span class="key">{{ label.0 }}</span> = <span class="value">{{ label.1
|
||||||
|
}}</span></dd>
|
||||||
|
{% endfor %}
|
||||||
|
{% when None %}
|
||||||
|
{% endmatch %}
|
||||||
</dl>
|
</dl>
|
||||||
</section>
|
</section>
|
||||||
<section class="log-section">
|
<section class="log-section">
|
||||||
|
@ -46,6 +69,10 @@
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
a.stack-name {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -68,8 +95,15 @@
|
||||||
color: #E796F3;
|
color: #E796F3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
|
||||||
|
& .key {
|
||||||
|
color: #E796F3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#log {
|
#log {
|
||||||
background-color: #171625;
|
background-color: var(--bg-raised);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -98,37 +132,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dl {
|
|
||||||
border: 1px solid #3D3E82;
|
|
||||||
background-color: #202248;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
dt,
|
|
||||||
dd {
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
dd~dt,
|
dd~dt,
|
||||||
dd~dd {
|
dd~dd {
|
||||||
border-top: 1px solid #262A65;
|
border-top: 1px solid #262A65;
|
||||||
}
|
}
|
||||||
|
|
||||||
dt {
|
|
||||||
float: left;
|
|
||||||
width: 25%;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
dd {
|
|
||||||
margin-left: 25%;
|
|
||||||
border-left: 1px dotted #3D3E82;
|
|
||||||
background-color: #171625;
|
|
||||||
padding: 4px 8px;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
dd[data-state] {
|
dd[data-state] {
|
||||||
background-color: #202248;
|
background-color: #202248;
|
||||||
}
|
}
|
||||||
|
@ -145,14 +153,63 @@
|
||||||
color: #FF9592;
|
color: #FF9592;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dt,
|
||||||
dd:after {
|
dd {
|
||||||
content: "";
|
box-sizing: border-box;
|
||||||
display: block;
|
padding: 4px;
|
||||||
clear: both;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main>h1 {
|
|
||||||
|
dt {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
background-color: #171625;
|
||||||
|
padding: 4px 8px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.table {
|
||||||
|
border: 1px solid #3D3E82;
|
||||||
|
background-color: #202248;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
& dt {
|
||||||
|
float: left;
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
& dd {
|
||||||
|
margin-left: 25%;
|
||||||
|
border-left: 1px dotted #3D3E82;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
& dd:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.list {
|
||||||
|
border: 1px solid #3D3E82;
|
||||||
|
background-color: #202248;
|
||||||
|
margin: 0;
|
||||||
|
border-top: 0;
|
||||||
|
|
||||||
|
& dd {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& dt {
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
main>header {
|
||||||
grid-area: head;
|
grid-area: head;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,10 +229,24 @@
|
||||||
"info log log"
|
"info log log"
|
||||||
"info log log";
|
"info log log";
|
||||||
|
|
||||||
grid-template-rows: 80px 1fr;
|
grid-template-rows: 120px 1fr;
|
||||||
grid-template-columns: 30vw 1fr;
|
grid-template-columns: 30vw 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
& form {
|
||||||
|
display: flex;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
|
@ -190,7 +261,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
const logEl = document.querySelector("#log");
|
const logEl = document.querySelector("#log");
|
||||||
const logSource = new EventSource(`${location.origin}${location.pathname}/log`);
|
const logSource = new EventSource(`${location.origin}${location.pathname}log`);
|
||||||
logSource.addEventListener("error", (ev) => {
|
logSource.addEventListener("error", (ev) => {
|
||||||
logSource.close();
|
logSource.close();
|
||||||
const err = document.createElement("div");
|
const err = document.createElement("div");
|
||||||
|
|
|
@ -4,13 +4,107 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main>
|
<main>
|
||||||
<h2>All stacks</h2>
|
<section class="system-info">
|
||||||
<ul>
|
<article>
|
||||||
{% for stack in stacks %}
|
<table>
|
||||||
<li class="{% if stack.active %}active{% endif %}"><a href="/stack/{{stack.name}}">{{stack.name}}</a>
|
<tbody>
|
||||||
</li>
|
<tr>
|
||||||
{% endfor %}
|
<th>Hostname</th>
|
||||||
</ul>
|
<td colspan="2">
|
||||||
|
{{ system.host_name }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>OS info</th>
|
||||||
|
<td colspan="2">
|
||||||
|
{{ system.name }} {{ system.os_version }} (kernel {{
|
||||||
|
system.kernel_version }})
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Load Avg</th>
|
||||||
|
<td colspan="2">
|
||||||
|
{{ system.load_average.0 }},
|
||||||
|
{{ system.load_average.1 }},
|
||||||
|
{{ system.load_average.2 }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Memory</th>
|
||||||
|
<td>
|
||||||
|
{{ system.used_memory|filesizeformat }}
|
||||||
|
/ {{ system.total_memory|filesizeformat }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<progress id="memory" value="{{system.used_memory}}"
|
||||||
|
max="{{system.total_memory}}">
|
||||||
|
{{
|
||||||
|
"{:.2}"|format(system.used_memory/system.total_memory*100)
|
||||||
|
}}%
|
||||||
|
</progress>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
{% for disk in system.disks %}
|
||||||
|
<tr>
|
||||||
|
<th title="{{disk.device_name}}">{{disk.mount_point}}</th>
|
||||||
|
<td>
|
||||||
|
{{ (disk.total_space-disk.available_space)|filesizeformat }}
|
||||||
|
/ {{ disk.total_space|filesizeformat }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<progress id="disk-{{disk.device_name}}"
|
||||||
|
value="{{disk.total_space-disk.available_space}}"
|
||||||
|
max="{{disk.total_space}}">
|
||||||
|
</progress>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
<section class="stack-list">
|
||||||
|
<h2>Stacks</h2>
|
||||||
|
<ul>
|
||||||
|
{% for stack in info.stacks %}
|
||||||
|
<li class="{% if stack.active %}active{% endif %}"><a
|
||||||
|
href="/stack/{{stack.name}}/">{{stack.name}}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section class="container-list">
|
||||||
|
<h2>Containers</h2>
|
||||||
|
<table class="containers">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Stack</th>
|
||||||
|
<th>State</th>
|
||||||
|
<th>Image</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for container in info.containers %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="/container/{{container.name}}/">{{container.name}}</a></td>
|
||||||
|
<td>{{container.stack().unwrap_or_default()}}</td>
|
||||||
|
<td class="status {{container.state}}">{{container.state}}</td>
|
||||||
|
<td>{{container.image}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -31,5 +125,71 @@
|
||||||
border-left-color: #33B074;
|
border-left-color: #33B074;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.containers {
|
||||||
|
background-color: var(--bg-raised);
|
||||||
|
border: 3px solid #5958B1;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
& th,
|
||||||
|
& td {
|
||||||
|
padding: 3px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& th {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
& thead {
|
||||||
|
background-color: #202248;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .status {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
padding: 0 10px;
|
||||||
|
|
||||||
|
&.exited {
|
||||||
|
background-color: #E5484D;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.running {
|
||||||
|
background-color: #46A758;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-list {
|
||||||
|
grid-area: con;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-list {
|
||||||
|
grid-area: stk;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-info {
|
||||||
|
grid-area: sys;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
& th {
|
||||||
|
font-weight: bold;
|
||||||
|
padding-right: 2ch;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1000px) {
|
||||||
|
main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas:
|
||||||
|
"sys sys"
|
||||||
|
"stk con";
|
||||||
|
|
||||||
|
grid-template-rows: 100px 1fr;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -4,28 +4,40 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main>
|
<main>
|
||||||
<h1>Stack details for <span class="stack-name">{{stack_name}}</span></h1>
|
<header>
|
||||||
<h2>Containers</h2>
|
<h1>Stack details for <span class="stack-name">{{stack_name}}</span></h1>
|
||||||
<table class="containers">
|
<div class="actions">
|
||||||
<thead>
|
<form action="./start" method="POST"><button type="submit">Start</button></form>
|
||||||
<tr>
|
<form action="./restart" method="POST"><button type="submit">Restart</button></form>
|
||||||
<th>Name</th>
|
<form action="./stop" method="POST"><button type="submit">Stop</button></form>
|
||||||
<th>State</th>
|
<form action="./down" method="POST"><button type="submit">Down</button></form>
|
||||||
<th>Image</th>
|
</div>
|
||||||
</tr>
|
</header>
|
||||||
</thead>
|
<section class="container-list">
|
||||||
<tbody>
|
<h2>Containers</h2>
|
||||||
{% for container in containers %}
|
<table class="containers">
|
||||||
<tr>
|
<thead>
|
||||||
<td><a href="/container/{{container.name}}">{{container.name}}</a></td>
|
<tr>
|
||||||
<td class="status {{container.state}}">{{container.state}}</td>
|
<th>Name</th>
|
||||||
<td>{{container.image}}</td>
|
<th>State</th>
|
||||||
</tr>
|
<th>Image</th>
|
||||||
{% endfor %}
|
</tr>
|
||||||
</tbody>
|
</thead>
|
||||||
</table>
|
<tbody>
|
||||||
<h2>Editor</h2>
|
{% for container in containers %}
|
||||||
<textarea id="editor">{{file_contents}}</textarea>
|
<tr>
|
||||||
|
<td><a href="/container/{{container.name}}/">{{container.name}}</a></td>
|
||||||
|
<td class="status {{container.state}}">{{container.state}}</td>
|
||||||
|
<td>{{container.image}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
<section class="editor">
|
||||||
|
<h2>Editor</h2>
|
||||||
|
<textarea id="editor">{{file_contents}}</textarea>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -37,8 +49,6 @@
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
|
@ -47,7 +57,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.containers {
|
.containers {
|
||||||
background-color: #171625;
|
background-color: var(--bg-raised);
|
||||||
border: 3px solid #5958B1;
|
border: 3px solid #5958B1;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
||||||
|
@ -68,6 +78,7 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: white;
|
color: white;
|
||||||
|
padding: 0 10px;
|
||||||
|
|
||||||
&.exited {
|
&.exited {
|
||||||
background-color: #E5484D;
|
background-color: #E5484D;
|
||||||
|
@ -88,6 +99,20 @@
|
||||||
border: 3px solid #5958B1;
|
border: 3px solid #5958B1;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
& form {
|
||||||
|
display: flex;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.31.2/ace.min.js"
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.31.2/ace.min.js"
|
||||||
|
|
Reference in a new issue