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"
|
||||
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
||||
|
||||
[[package]]
|
||||
name = "countme"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.3"
|
||||
|
@ -990,12 +996,40 @@ dependencies = [
|
|||
"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]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.14"
|
||||
|
@ -1159,6 +1193,7 @@ dependencies = [
|
|||
"clap",
|
||||
"dotenvy",
|
||||
"futures-util",
|
||||
"rnix",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sysinfo",
|
||||
|
@ -1209,6 +1244,12 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "text-size"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.50"
|
||||
|
|
|
@ -12,6 +12,7 @@ bollard = { version = "0.15", features = ["time"] }
|
|||
clap = { version = "4", features = ["env", "derive"] }
|
||||
dotenvy = "0.15"
|
||||
futures-util = "0.3"
|
||||
rnix = "0.11.0"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
sysinfo = "0.29.10"
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
use crate::http::response::response_interceptor;
|
||||
use anyhow::Result;
|
||||
use axum::middleware::from_fn;
|
||||
use bollard::Docker;
|
||||
use clap::Parser;
|
||||
use std::{net::SocketAddr, path::PathBuf};
|
||||
|
||||
use crate::http::response::response_interceptor;
|
||||
|
||||
mod http;
|
||||
mod nix;
|
||||
mod node;
|
||||
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 bollard::{
|
||||
container::{
|
||||
|
@ -10,6 +8,7 @@ use bollard::{
|
|||
Docker,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
@ -31,6 +30,10 @@ impl ContainerInfo {
|
|||
.clone()
|
||||
.and_then(|lab| lab.get("com.docker.compose.project").cloned())
|
||||
}
|
||||
|
||||
pub fn running(&self) -> bool {
|
||||
self.state == "running"
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ContainerSummary> for ContainerInfo {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
pub mod container;
|
||||
pub mod error;
|
||||
pub mod nix;
|
||||
pub mod stack;
|
||||
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 bollard::{container::ListContainersOptions, service::ContainerSummary, Docker};
|
||||
use serde::Serialize;
|
||||
|
@ -36,10 +40,25 @@ pub async fn get_compose(base_dir: &Path, stack_name: &str) -> Result<String> {
|
|||
Ok(contents)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ServiceInfo {
|
||||
name: String,
|
||||
container: Option<String>,
|
||||
running: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct StackInfo {
|
||||
pub name: String,
|
||||
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)]
|
||||
|
@ -48,6 +67,27 @@ pub struct NodeInfo {
|
|||
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> {
|
||||
let containers: Vec<ContainerInfo> = docker
|
||||
.list_containers(Some(ListContainersOptions::<String> {
|
||||
|
@ -73,7 +113,18 @@ pub async fn list(base_dir: &Path, docker: &Docker) -> Result<NodeInfo> {
|
|||
let active = containers
|
||||
.iter()
|
||||
.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::{
|
||||
http::{
|
||||
accept::ExtractAccept,
|
||||
|
@ -23,6 +21,7 @@ use axum::{
|
|||
use bollard::container::LogsOptions;
|
||||
use futures_util::stream::StreamExt;
|
||||
use serde_json::json;
|
||||
use std::convert::Infallible;
|
||||
|
||||
#[derive(Template)]
|
||||
#[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::{
|
||||
http::{
|
||||
error::AppError,
|
||||
|
@ -19,6 +9,15 @@ use crate::{
|
|||
},
|
||||
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)]
|
||||
#[template(path = "stack/get-one.html")]
|
||||
|
|
|
@ -76,8 +76,15 @@
|
|||
<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 class="{% if stack.active %}active{% endif %}">
|
||||
<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>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
@ -180,6 +187,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
.count {
|
||||
&::before {
|
||||
content: "◉";
|
||||
margin-right: 0.5ch;
|
||||
}
|
||||
|
||||
&.running::before {
|
||||
color: #46A758;
|
||||
}
|
||||
|
||||
&.stopped::before {
|
||||
color: #E5484D;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
main {
|
||||
display: grid;
|
||||
|
|
Reference in a new issue