A focused Docker Compose management web application.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: container statuses

Brooke 3a5e32f2 39f38b5a

+162 -29
+47
packages/macros/Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "luminary-macros" 7 + version = "0.0.0" 8 + dependencies = [ 9 + "proc-macro2", 10 + "quote", 11 + "syn", 12 + ] 13 + 14 + [[package]] 15 + name = "proc-macro2" 16 + version = "1.0.106" 17 + source = "registry+https://github.com/rust-lang/crates.io-index" 18 + checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" 19 + dependencies = [ 20 + "unicode-ident", 21 + ] 22 + 23 + [[package]] 24 + name = "quote" 25 + version = "1.0.44" 26 + source = "registry+https://github.com/rust-lang/crates.io-index" 27 + checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" 28 + dependencies = [ 29 + "proc-macro2", 30 + ] 31 + 32 + [[package]] 33 + name = "syn" 34 + version = "2.0.116" 35 + source = "registry+https://github.com/rust-lang/crates.io-index" 36 + checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" 37 + dependencies = [ 38 + "proc-macro2", 39 + "quote", 40 + "unicode-ident", 41 + ] 42 + 43 + [[package]] 44 + name = "unicode-ident" 45 + version = "1.0.24" 46 + source = "registry+https://github.com/rust-lang/crates.io-index" 47 + checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+1 -1
packages/macros/Cargo.toml
··· 5 5 [dependencies] 6 6 proc-macro2 = "1.0" 7 7 quote = "1.0.40" 8 - syn = "2.0.101" 8 + syn = { version = "2.0.101", features = ["full"] } 9 9 10 10 [lib] 11 11 proc-macro = true
+2
packages/macros/src/lib.rs
··· 9 9 let func = parse_macro_input!(item as ItemFn); 10 10 let attr: TokenStream2 = attr.into(); 11 11 let visibility = &func.vis; 12 + let attrs = &func.attrs; 12 13 13 14 // Create variables for quoting 14 15 let asyncness = &func.sig.asyncness; ··· 22 23 }; 23 24 24 25 return quote! { 26 + #(#attrs)* 25 27 #visibility #signature { 26 28 color_eyre::eyre::WrapErr::wrap_err((#asyncness move || #output #block)()#wait, #attr) 27 29 }
+3
packages/node/src/core/mod.rs
··· 9 9 mod model; 10 10 mod project; 11 11 12 + /// A struct containing Luminary configuration, to be loaded from environment variables. 12 13 #[derive(Deserialize, Debug)] 13 14 pub struct LuminaryConfiguration { 14 15 pub project_directory: String, 15 16 } 16 17 18 + /// The core struct of the Luminary application, containing shared state and configuration. 17 19 #[derive(Debug)] 18 20 pub struct LuminaryCore { 19 21 pub configuration: LuminaryConfiguration, ··· 21 23 } 22 24 23 25 impl LuminaryCore { 26 + /// Initializes a new instance of the LuminaryCore struct, loading configuration from environment variables and connecting to the Docker engine. 24 27 pub fn new() -> Result<Self> { 25 28 let docker = Docker::connect_with_defaults().wrap_err("Failed to connect to docker engine.")?; 26 29 let configuration = envy::prefixed("LUMINARY_").from_env::<LuminaryConfiguration>()?;
+30
packages/node/src/core/model.rs
··· 1 + //! This module defines the core data models used within the Luminary application. 2 + 1 3 use std::collections::HashMap; 2 4 3 5 use serde::{Deserialize, Serialize}; 4 6 use specta::Type; 5 7 8 + /// Represents a Luminary project, consisting of a docker compose project. 6 9 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Type)] 10 + #[serde(rename_all = "camelCase")] 7 11 pub struct LuminaryProject { 8 12 pub name: String, 13 + pub status: LuminaryStatus, 9 14 pub services: HashMap<String, LuminaryService>, 10 15 } 11 16 17 + /// Represents a service within a Luminary project. 12 18 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Type)] 19 + #[serde(rename_all = "camelCase")] 13 20 pub struct LuminaryService { 14 21 pub name: String, 22 + pub status: LuminaryStatus, 23 + } 24 + 25 + /// Represents the various possible activity statuses of a Luminary service. 26 + /// Variants are ordered from lowest (Exited) to highest (Healthy). 27 + #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Type)] 28 + #[serde(rename_all = "camelCase")] 29 + pub enum LuminaryStatus { 30 + Exited, 31 + Down, 32 + Paused, 33 + Restarting, 34 + Removing, 35 + Starting, 36 + Running, 37 + Healthy, 38 + } 39 + 40 + impl LuminaryStatus { 41 + /// Returns the lowest status from an iterator, or `None` if empty. 42 + pub fn min(statuses: impl IntoIterator<Item = Self>) -> Self { 43 + statuses.into_iter().min().unwrap_or(LuminaryStatus::Down) 44 + } 15 45 }
+71 -24
packages/node/src/core/project.rs
··· 1 + //! This module implements the core logic for managing Luminary projects. 2 + 1 3 use std::{collections::HashMap, path::Path}; 2 4 3 - use bollard::query_parameters::ListContainersOptionsBuilder; 4 - use color_eyre::eyre::{ContextCompat, Ok, Result, WrapErr}; 5 + use bollard::{query_parameters::ListContainersOptionsBuilder, secret::ContainerSummaryStateEnum}; 6 + use color_eyre::eyre::{Ok, Result, WrapErr}; 5 7 use docker_compose_types::Compose; 6 8 use luminary_macros::wrap_err; 7 9 use tokio::fs::{self, File}; 8 10 9 11 use crate::core::{ 10 12 LuminaryCore, 11 - model::{LuminaryProject, LuminaryService}, 13 + model::{LuminaryProject, LuminaryService, LuminaryStatus}, 12 14 }; 13 15 14 16 const COMPOSE_PROJECT_DIR_LABEL: &str = "com.docker.compose.project.working_dir"; ··· 18 20 const COMPOSE_FILENAME: &str = "compose.yml"; 19 21 20 22 impl LuminaryCore { 23 + /// Lists all Luminary projects by combining data from both the filesystem and Docker engine. 24 + #[wrap_err("Failed to list projects")] 25 + pub async fn list_projects(&self) -> Result<HashMap<String, LuminaryProject>> { 26 + let disk_projects = self.list_from_filesystem().await?; 27 + let mut projects = self.list_from_docker().await?; 28 + 29 + for (name, disk) in disk_projects { 30 + let project = projects.entry(name).or_insert(disk); 31 + project.status = LuminaryStatus::min(project.services.values().map(|s| s.status)); 32 + } 33 + 34 + return Ok(projects); 35 + } 36 + 37 + /// Lists all Luminary projects found in the configured projects directory. 21 38 #[wrap_err("Failed to list projects from filesystem")] 22 39 async fn list_from_filesystem(&self) -> Result<HashMap<String, LuminaryProject>> { 23 40 let mut projects = HashMap::<String, LuminaryProject>::new(); ··· 27 44 .wrap_err("Failed to list project directory contents")?; 28 45 while let Some(entry) = entries.next_entry().await? { 29 46 let mut path = entry.path(); 30 - if path.is_dir() { 31 - let project_name = path 32 - .file_name() 33 - .wrap_err("Failed to get project directory name")? 34 - .to_str() 35 - .wrap_err("Failed to read project directory name")? 36 - .to_owned(); 47 + if path.is_dir() 48 + && let Some(project_name) = path.file_name().and_then(|n| n.to_str()).map(|s| s.to_string()) 49 + { 37 50 path.push(COMPOSE_FILENAME); 38 51 if path.exists() { 39 52 let file = File::open(path).await.wrap_err("Failed to open compose file")?; 40 53 let compose: Compose = serde_saphyr::from_reader(file.into_std().await)?; 41 - 42 - projects.insert( 43 - project_name.clone(), 44 - LuminaryProject { 45 - name: project_name, 46 - services: compose 47 - .services 48 - .0 49 - .into_iter() 50 - .map(|(name, _)| (name.clone(), LuminaryService { name: name.clone() })) 51 - .collect(), 52 - }, 53 - ); 54 + projects.insert(project_name.clone(), self.parse_compose(project_name, compose)); 54 55 } 55 56 } 56 57 } ··· 58 59 return Ok(projects); 59 60 } 60 61 62 + /// Parses a given compose file into a LuminaryProject struct, initializing all services with a default status of "Down". 63 + fn parse_compose(&self, name: String, compose: Compose) -> LuminaryProject { 64 + LuminaryProject { 65 + name, 66 + status: LuminaryStatus::Down, 67 + services: compose 68 + .services 69 + .0 70 + .into_iter() 71 + .map(|(name, _)| { 72 + ( 73 + name.clone(), 74 + LuminaryService { 75 + name: name.clone(), 76 + status: LuminaryStatus::Down, 77 + }, 78 + ) 79 + }) 80 + .collect(), 81 + } 82 + } 83 + 84 + /// Lists all Luminary projects by querying the Docker engine for containers with specific labels. 61 85 #[wrap_err("Failed to list projects from docker engine")] 62 86 async fn list_from_docker(&self) -> Result<HashMap<String, LuminaryProject>> { 63 87 let options = ListContainersOptionsBuilder::default().all(true).build(); ··· 74 98 && let Some(dir) = labels.remove(COMPOSE_PROJECT_DIR_LABEL) 75 99 { 76 100 if Path::new(&dir).starts_with(&self.configuration.project_directory) { 101 + let status = self.parse_state(container.state); 77 102 acc.entry(project.clone()) 78 103 .or_insert_with(|| LuminaryProject { 79 104 services: HashMap::new(), 105 + status: status, 80 106 name: project, 81 107 }) 82 108 .services 83 - .insert(service.clone(), LuminaryService { name: service }); 109 + .insert( 110 + service.clone(), 111 + LuminaryService { 112 + name: service, 113 + status, 114 + }, 115 + ); 84 116 } 85 117 } 86 118 ··· 88 120 }); 89 121 90 122 return Ok(projects); 123 + } 124 + 125 + /// Parses a Docker container state into a corresponding LuminaryStatus. 126 + fn parse_state(&self, state: Option<ContainerSummaryStateEnum>) -> LuminaryStatus { 127 + return match state { 128 + Some(ContainerSummaryStateEnum::CREATED) => LuminaryStatus::Starting, 129 + Some(ContainerSummaryStateEnum::RUNNING) => LuminaryStatus::Running, 130 + Some(ContainerSummaryStateEnum::PAUSED) => LuminaryStatus::Paused, 131 + Some(ContainerSummaryStateEnum::RESTARTING) => LuminaryStatus::Restarting, 132 + Some(ContainerSummaryStateEnum::EXITED) => LuminaryStatus::Exited, 133 + Some(ContainerSummaryStateEnum::REMOVING) => LuminaryStatus::Removing, 134 + Some(ContainerSummaryStateEnum::EMPTY) => LuminaryStatus::Down, 135 + Some(ContainerSummaryStateEnum::DEAD) => LuminaryStatus::Down, 136 + None => LuminaryStatus::Down, 137 + }; 91 138 } 92 139 }
+8 -4
packages/node/src/main.rs
··· 10 10 async fn main() -> Result<()> { 11 11 dotenv().ok(); 12 12 13 - let listener = TcpListener::bind("0.0.0.0:9000").await?; 14 - let router = Router::new().nest("/api/", api::router()); 13 + let core = core::LuminaryCore::new()?; 14 + let projects = core.list_projects().await?; 15 + println!("Projects: {:#?}", projects); 15 16 16 - println!("Listening on http://127.0.0.1:{}", listener.local_addr()?.port()); 17 - axum::serve(listener, router).await?; 17 + // let listener = TcpListener::bind("0.0.0.0:9000").await?; 18 + // let router = Router::new().nest("/api/", api::router()); 19 + 20 + // println!("Listening on http://127.0.0.1:{}", listener.local_addr()?.port()); 21 + // axum::serve(listener, router).await?; 18 22 return Ok(()); 19 23 }