diff --git a/src/node/container.rs b/src/node/container.rs index c657b7c..72e62fe 100644 --- a/src/node/container.rs +++ b/src/node/container.rs @@ -8,7 +8,7 @@ use bollard::{ Docker, }; use serde::Serialize; -use std::collections::HashMap; +use std::{collections::HashMap, path::Path}; use time::OffsetDateTime; #[derive(Serialize)] @@ -31,6 +31,19 @@ impl ContainerInfo { .and_then(|lab| lab.get("com.docker.compose.project").cloned()) } + pub fn stack_folder(&self) -> Option { + self.labels + .clone() + .and_then(|lab| lab.get("com.docker.compose.project.working_dir").cloned()) + .and_then(|workdir| { + Path::new(&workdir) + .components() + .last() + .and_then(|part| part.as_os_str().to_str()) + .map(|s| s.to_string()) + }) + } + pub fn running(&self) -> bool { self.state == "running" } diff --git a/src/node/nix.rs b/src/node/nix.rs index 43bcf9b..18081a8 100644 --- a/src/node/nix.rs +++ b/src/node/nix.rs @@ -3,6 +3,7 @@ use anyhow::{anyhow, Result}; use std::collections::HashSet; pub struct StackComposeInfo { + pub project: String, pub services: Vec, } @@ -11,6 +12,11 @@ pub fn parse_arion_compose(file: &str) -> Result { let expr = ast.expr().ok_or_else(|| anyhow!("invalid nix root"))?; let flattened = flatten_set(expr)?; + let project = flattened["project\0name"] + .clone() + .trim_matches('"') + .to_string(); + let services = flattened .iter() .filter_map(|(key, _)| { @@ -22,5 +28,43 @@ pub fn parse_arion_compose(file: &str) -> Result { .into_iter() .collect(); - Ok(StackComposeInfo { services }) + Ok(StackComposeInfo { project, services }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_compose() { + let source = r#"{ + project.name = "traefik"; + services.router = { + service.image = "traefik:v3.0"; + service.restart = "always"; + service.volumes = [ + "${toString ./.}/traefik.toml:/traefik.toml" + "${toString ./.}/acme.json:/acme.json" + "${toString ./.}/rules.d:/rules.d" + "/var/run/docker.sock:/var/run/docker.sock:ro" + ]; + service.ports = [ + "80:80" + ]; + service.labels = { + "traefik.enable" = "true"; + "traefik.http.routers.traefik_dashboard.rule" = "Host(`traefik.uchunext.local`)"; + "traefik.http.routers.traefik_dashboard.service" = "api@internal"; + "traefik.http.routers.traefik_dashboard.entrypoints" = "web"; + "traefik.http.services.dashboard.loadbalancer.server.port" = "8080"; + }; + service.networks = ["web"]; + }; + networks.web.external = true; + }"#; + let info = parse_arion_compose(source).unwrap(); + + assert_eq!(info.project, "traefik"); + assert_eq!(info.services[0], "router"); + } } diff --git a/src/node/stack.rs b/src/node/stack.rs index 241a617..df5e0f8 100644 --- a/src/node/stack.rs +++ b/src/node/stack.rs @@ -57,6 +57,7 @@ pub struct ServiceInfo { #[derive(Serialize)] pub struct StackInfo { + pub folder: String, pub name: String, pub active: bool, pub services: Vec, @@ -116,19 +117,21 @@ pub async fn list(base_dir: &Path, docker: &Docker) -> Result { continue; } if is_stack(&dir.path()).await? { - let name = dir.file_name().to_string_lossy().to_string(); + let folder = dir.file_name().to_string_lossy().to_string(); + let compose_file = get_compose(base_dir, &folder).await?; + let info = parse_arion_compose(&compose_file)?; + let name = info.project; // Check status by analyzing containers let active = containers .iter() .any(|cont| cont.state == "running" && cont.stack() == Some(name.clone())); - 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 { + folder, name, active, services, @@ -207,15 +210,13 @@ pub async fn remove( Ok(()) } -pub async fn check_compose(arion_bin: &Path, source: &str) -> Result<()> { +pub async fn check_compose(arion_bin: &Path, source: &str) -> Result { // Check that it's a valid nix tree - rnix::Root::parse(source) - .ok() - .map_err(|err| AppError::Client { - status: StatusCode::NOT_ACCEPTABLE, - code: "failed-nix-parse", - message: format!("Parse error: {}", err), - })?; + let info = parse_arion_compose(source).map_err(|err| AppError::Client { + status: StatusCode::NOT_ACCEPTABLE, + code: "failed-nix-parse", + message: format!("Parse error: {}", err), + })?; // Create a temporary stack and check that it generates a YAML tree let dir = tempdir()?; @@ -239,7 +240,7 @@ pub async fn check_compose(arion_bin: &Path, source: &str) -> Result<()> { message: format!("Arion {}", err), }) } else { - Ok(()) + Ok(info.project) } } diff --git a/src/route/stack.rs b/src/route/stack.rs index 44733e3..51dcb48 100644 --- a/src/route/stack.rs +++ b/src/route/stack.rs @@ -5,6 +5,7 @@ use crate::{ }, node::{ container::ContainerInfo, + nix::parse_arion_compose, stack::{ check_compose, command, commit_compose, create_new, get_compose, get_containers, remove, write_compose, StackCommand, @@ -28,6 +29,7 @@ use serde_json::json; #[derive(Template)] #[template(path = "stack/get-one.html")] struct GetOneTemplate { + stack_folder: String, stack_name: String, file_contents: String, containers: Vec, @@ -44,22 +46,20 @@ struct ConfirmDeleteTemplate { } async fn get_one(Path(stack_name): Path, State(state): State) -> HandlerResponse { - let (file_contents_res, containers_res) = join!( - get_compose(&state.stack_dir, &stack_name), - get_containers(&state.docker, &stack_name) - ); - - let file_contents = file_contents_res?; - let containers = containers_res?; + let file_contents = get_compose(&state.stack_dir, &stack_name).await?; + let info = parse_arion_compose(&file_contents)?; + let containers = get_containers(&state.docker, &info.project).await?; reply( json!({ - "name": stack_name, + "folder": stack_name, + "name": info.project, "file": file_contents, "containers": containers, }), GetOneTemplate { - stack_name, + stack_folder: stack_name, + stack_name: info.project, file_contents, containers: containers.iter().map(|c| c.clone().into()).collect(), }, diff --git a/templates/container/get-one.html b/templates/container/get-one.html index 4f5fd39..b541edf 100644 --- a/templates/container/get-one.html +++ b/templates/container/get-one.html @@ -7,7 +7,10 @@

Container {{container_name}} {% match info.stack() %}{% when Some - with (stack) %}({{stack}}) + with (stack) %}({% match info.stack_folder() %}{% when + Some + with (stack_folder) %}{{stack}}) + {% when None %}{% endmatch %} {% when None %}{% endmatch %}

diff --git a/templates/home.html b/templates/home.html index 9cdb935..be15733 100644 --- a/templates/home.html +++ b/templates/home.html @@ -87,7 +87,7 @@ - + {{stack.name}} diff --git a/templates/stack/new-form.html b/templates/stack/new-form.html index 13e46cc..c7c6b6f 100644 --- a/templates/stack/new-form.html +++ b/templates/stack/new-form.html @@ -9,9 +9,6 @@
-
- -