I'm a nix parser now

This commit is contained in:
Hamcha 2023-11-21 13:06:31 +01:00
parent ae6078e8a7
commit 40202f6889
12 changed files with 255 additions and 20 deletions

41
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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
View file

@ -0,0 +1 @@
pub mod parse;

91
src/nix/parse.rs Normal file
View 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"#);
}
}

View file

@ -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 {

View file

@ -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
View 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 })
}

View file

@ -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,
})
} }
} }

View file

@ -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")]

View file

@ -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")]

View file

@ -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;