I'm a nix parser now
This commit is contained in:
parent
ae6078e8a7
commit
40202f6889
12 changed files with 255 additions and 20 deletions
41
Cargo.lock
generated
41
Cargo.lock
generated
|
@ -377,6 +377,12 @@ 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 = "countme"
|
||||||
|
version = "3.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-deque"
|
name = "crossbeam-deque"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
|
@ -990,12 +996,40 @@ dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rnix"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bb35cedbeb70e0ccabef2a31bcff0aebd114f19566086300b8f42c725fc2cb5f"
|
||||||
|
dependencies = [
|
||||||
|
"rowan",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rowan"
|
||||||
|
version = "0.15.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "906057e449592587bf6724f00155bf82a6752c868d78a8fb3aa41f4e6357cfe8"
|
||||||
|
dependencies = [
|
||||||
|
"countme",
|
||||||
|
"hashbrown 0.12.3",
|
||||||
|
"memoffset",
|
||||||
|
"rustc-hash",
|
||||||
|
"text-size",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.23"
|
version = "0.1.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
|
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.14"
|
version = "1.0.14"
|
||||||
|
@ -1159,6 +1193,7 @@ dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"rnix",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
|
@ -1209,6 +1244,12 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "text-size"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.50"
|
version = "1.0.50"
|
||||||
|
|
|
@ -12,6 +12,7 @@ bollard = { version = "0.15", features = ["time"] }
|
||||||
clap = { version = "4", features = ["env", "derive"] }
|
clap = { version = "4", features = ["env", "derive"] }
|
||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
|
rnix = "0.11.0"
|
||||||
serde = "1"
|
serde = "1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
sysinfo = "0.29.10"
|
sysinfo = "0.29.10"
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
|
use crate::http::response::response_interceptor;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use axum::middleware::from_fn;
|
use axum::middleware::from_fn;
|
||||||
use bollard::Docker;
|
use bollard::Docker;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use std::{net::SocketAddr, path::PathBuf};
|
use std::{net::SocketAddr, path::PathBuf};
|
||||||
|
|
||||||
use crate::http::response::response_interceptor;
|
|
||||||
|
|
||||||
mod http;
|
mod http;
|
||||||
|
mod nix;
|
||||||
mod node;
|
mod node;
|
||||||
mod route;
|
mod route;
|
||||||
|
|
||||||
|
|
1
src/nix/mod.rs
Normal file
1
src/nix/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod parse;
|
91
src/nix/parse.rs
Normal file
91
src/nix/parse.rs
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use rnix::ast::Entry;
|
||||||
|
use rnix::{ast::Expr, ast::HasEntry};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub type FlattenedSet = HashMap<String, String>;
|
||||||
|
|
||||||
|
pub fn flatten_set(expr: Expr) -> Result<FlattenedSet> {
|
||||||
|
let set = match expr {
|
||||||
|
Expr::AttrSet(set) => set,
|
||||||
|
_ => return Err(anyhow!("not a set")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut hashmap = HashMap::new();
|
||||||
|
|
||||||
|
for entry in set.entries() {
|
||||||
|
match entry {
|
||||||
|
Entry::AttrpathValue(val) => {
|
||||||
|
let path = val
|
||||||
|
.attrpath()
|
||||||
|
.ok_or_else(|| anyhow!("could not read attr"))?;
|
||||||
|
let attr = path
|
||||||
|
.attrs()
|
||||||
|
.map(|ast| ast.to_string())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("\0");
|
||||||
|
match val.value().map(flatten_set).and_then(|res| res.ok()) {
|
||||||
|
Some(nested_map) => {
|
||||||
|
for (entry, value) in nested_map {
|
||||||
|
hashmap.insert(format!("{}\0{}", attr, entry), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
hashmap
|
||||||
|
.insert(attr, val.value().map(|v| v.to_string()).unwrap_or_default());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(hashmap)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use rnix::Root;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_flatten_set_simple() {
|
||||||
|
let test_obj = r#"{
|
||||||
|
foo = {
|
||||||
|
bar = "baz";
|
||||||
|
};
|
||||||
|
}"#;
|
||||||
|
let ast = Root::parse(test_obj).ok().expect("invalid nix source");
|
||||||
|
let expr = ast.expr().expect("invalid nix root");
|
||||||
|
let flattened = flatten_set(expr).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(flattened.get("foo\0bar").unwrap(), r#""baz""#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_flatten_set_multiple_levels() {
|
||||||
|
let test_obj = r#"{
|
||||||
|
foo = {
|
||||||
|
bar = {
|
||||||
|
baz = "qux";
|
||||||
|
};
|
||||||
|
bar.qux = "quux";
|
||||||
|
};
|
||||||
|
foo.boo = {
|
||||||
|
bar = {
|
||||||
|
bax = "qxx";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
foo.fii.fuu = 123;
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let ast = Root::parse(test_obj).ok().expect("invalid nix source");
|
||||||
|
let expr = ast.expr().expect("invalid nix root");
|
||||||
|
let flattened = flatten_set(expr).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(flattened.get("foo\0bar\0baz").unwrap(), r#""qux""#);
|
||||||
|
assert_eq!(flattened.get("foo\0bar\0qux").unwrap(), r#""quux""#);
|
||||||
|
assert_eq!(flattened.get("foo\0boo\0bar\0bax").unwrap(), r#""qxx""#);
|
||||||
|
assert_eq!(flattened.get("foo\0fii\0fuu").unwrap(), r#"123"#);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,3 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use crate::http::error::Result;
|
use crate::http::error::Result;
|
||||||
use bollard::{
|
use bollard::{
|
||||||
container::{
|
container::{
|
||||||
|
@ -10,6 +8,7 @@ use bollard::{
|
||||||
Docker,
|
Docker,
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
@ -31,6 +30,10 @@ impl ContainerInfo {
|
||||||
.clone()
|
.clone()
|
||||||
.and_then(|lab| lab.get("com.docker.compose.project").cloned())
|
.and_then(|lab| lab.get("com.docker.compose.project").cloned())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn running(&self) -> bool {
|
||||||
|
self.state == "running"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ContainerSummary> for ContainerInfo {
|
impl From<ContainerSummary> for ContainerInfo {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod container;
|
pub mod container;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod nix;
|
||||||
pub mod stack;
|
pub mod stack;
|
||||||
pub mod system;
|
pub mod system;
|
||||||
|
|
26
src/node/nix.rs
Normal file
26
src/node/nix.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
use crate::nix::parse::flatten_set;
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
pub struct StackComposeInfo {
|
||||||
|
pub services: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_arion_compose(file: &str) -> Result<StackComposeInfo> {
|
||||||
|
let ast = rnix::Root::parse(file).ok()?;
|
||||||
|
let expr = ast.expr().ok_or_else(|| anyhow!("invalid nix root"))?;
|
||||||
|
let flattened = flatten_set(expr)?;
|
||||||
|
|
||||||
|
let services = flattened
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(key, _)| {
|
||||||
|
key.strip_prefix("services\0")
|
||||||
|
.and_then(|key| key.split_once('\0'))
|
||||||
|
.and_then(|k| Some(k.0.to_string()))
|
||||||
|
})
|
||||||
|
.collect::<HashSet<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(StackComposeInfo { services })
|
||||||
|
}
|
|
@ -1,4 +1,8 @@
|
||||||
use super::{container::ContainerInfo, error::StackError};
|
use super::{
|
||||||
|
container::ContainerInfo,
|
||||||
|
error::StackError,
|
||||||
|
nix::{parse_arion_compose, StackComposeInfo},
|
||||||
|
};
|
||||||
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;
|
||||||
|
@ -36,10 +40,25 @@ pub async fn get_compose(base_dir: &Path, stack_name: &str) -> Result<String> {
|
||||||
Ok(contents)
|
Ok(contents)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ServiceInfo {
|
||||||
|
name: String,
|
||||||
|
container: Option<String>,
|
||||||
|
running: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct StackInfo {
|
pub struct StackInfo {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
|
pub services: Vec<ServiceInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StackInfo {
|
||||||
|
pub fn stats(&self) -> (usize, usize) {
|
||||||
|
let running = self.services.iter().filter(|s| s.running).count();
|
||||||
|
(running, self.services.len() - running)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
@ -48,6 +67,27 @@ pub struct NodeInfo {
|
||||||
pub containers: Vec<ContainerInfo>,
|
pub containers: Vec<ContainerInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_service(containers: &Vec<ContainerInfo>, stack_name: &str, service: &str) -> ServiceInfo {
|
||||||
|
let container = containers.iter().find(|cont| {
|
||||||
|
let labels = cont.labels.clone().unwrap_or_default();
|
||||||
|
labels.get("com.docker.compose.project") == Some(&stack_name.to_string())
|
||||||
|
&& labels.get("com.docker.compose.service") == Some(&service.to_string())
|
||||||
|
});
|
||||||
|
|
||||||
|
match container {
|
||||||
|
Some(info) => ServiceInfo {
|
||||||
|
name: service.to_string(),
|
||||||
|
container: Some(info.name.clone()),
|
||||||
|
running: info.running(),
|
||||||
|
},
|
||||||
|
_ => ServiceInfo {
|
||||||
|
name: service.to_string(),
|
||||||
|
container: None,
|
||||||
|
running: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn list(base_dir: &Path, docker: &Docker) -> Result<NodeInfo> {
|
pub async fn list(base_dir: &Path, docker: &Docker) -> Result<NodeInfo> {
|
||||||
let containers: Vec<ContainerInfo> = docker
|
let containers: Vec<ContainerInfo> = docker
|
||||||
.list_containers(Some(ListContainersOptions::<String> {
|
.list_containers(Some(ListContainersOptions::<String> {
|
||||||
|
@ -73,7 +113,18 @@ pub async fn list(base_dir: &Path, docker: &Docker) -> Result<NodeInfo> {
|
||||||
let active = containers
|
let active = containers
|
||||||
.iter()
|
.iter()
|
||||||
.any(|cont| cont.state == "running" && cont.stack() == Some(name.clone()));
|
.any(|cont| cont.state == "running" && cont.stack() == Some(name.clone()));
|
||||||
stacks.push(StackInfo { name, active })
|
let compose_file = get_compose(base_dir, &name).await?;
|
||||||
|
let info = parse_arion_compose(&compose_file)?;
|
||||||
|
let services = info
|
||||||
|
.services
|
||||||
|
.iter()
|
||||||
|
.map(|service| get_service(&containers, &name, service))
|
||||||
|
.collect();
|
||||||
|
stacks.push(StackInfo {
|
||||||
|
name,
|
||||||
|
active,
|
||||||
|
services,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
use std::convert::Infallible;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
http::{
|
http::{
|
||||||
accept::ExtractAccept,
|
accept::ExtractAccept,
|
||||||
|
@ -23,6 +21,7 @@ use axum::{
|
||||||
use bollard::container::LogsOptions;
|
use bollard::container::LogsOptions;
|
||||||
use futures_util::stream::StreamExt;
|
use futures_util::stream::StreamExt;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use std::convert::Infallible;
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "container/get-one.html")]
|
#[template(path = "container/get-one.html")]
|
||||||
|
|
|
@ -1,13 +1,3 @@
|
||||||
use askama::Template;
|
|
||||||
use axum::{
|
|
||||||
extract::{Path, State},
|
|
||||||
response::Redirect,
|
|
||||||
routing::{get, post},
|
|
||||||
Router,
|
|
||||||
};
|
|
||||||
use futures_util::join;
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
http::{
|
http::{
|
||||||
error::AppError,
|
error::AppError,
|
||||||
|
@ -19,6 +9,15 @@ use crate::{
|
||||||
},
|
},
|
||||||
AppState,
|
AppState,
|
||||||
};
|
};
|
||||||
|
use askama::Template;
|
||||||
|
use axum::{
|
||||||
|
extract::{Path, State},
|
||||||
|
response::Redirect,
|
||||||
|
routing::{get, post},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use futures_util::join;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "stack/get-one.html")]
|
#[template(path = "stack/get-one.html")]
|
||||||
|
|
|
@ -76,8 +76,15 @@
|
||||||
<h2>Stacks</h2>
|
<h2>Stacks</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{% for stack in info.stacks %}
|
{% for stack in info.stacks %}
|
||||||
<li class="{% if stack.active %}active{% endif %}"><a
|
<li class="{% if stack.active %}active{% endif %}">
|
||||||
href="/stack/{{stack.name}}/">{{stack.name}}</a>
|
<a href="/stack/{{stack.name}}/">{{stack.name}}</a>
|
||||||
|
{% let (running, stopped) = stack.stats() %}
|
||||||
|
{% if running > 0 %}
|
||||||
|
<span class="count running">{{running}}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if stopped > 0 %}
|
||||||
|
<span class="count stopped">{{stopped}}</span>
|
||||||
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -180,6 +187,21 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
&::before {
|
||||||
|
content: "◉";
|
||||||
|
margin-right: 0.5ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.running::before {
|
||||||
|
color: #46A758;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.stopped::before {
|
||||||
|
color: #E5484D;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 1000px) {
|
@media (min-width: 1000px) {
|
||||||
main {
|
main {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
Reference in a new issue