A focused Docker Compose management web application.
0
fork

Configure Feed

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

feat: list projects from disk

Brooke 39f38b5a 2583a4b8

+68 -26
+1
.env.example
··· 1 + LUMINARY_PROJECT_DIRECTORY=/opt/stacks # Directory where Luminary will look for projects.
+17
packages/node/Cargo.lock
··· 378 378 ] 379 379 380 380 [[package]] 381 + name = "dotenv" 382 + version = "0.15.0" 383 + source = "registry+https://github.com/rust-lang/crates.io-index" 384 + checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" 385 + 386 + [[package]] 381 387 name = "either" 382 388 version = "1.15.0" 383 389 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 399 405 checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" 400 406 dependencies = [ 401 407 "encoding_rs", 408 + ] 409 + 410 + [[package]] 411 + name = "envy" 412 + version = "0.4.2" 413 + source = "registry+https://github.com/rust-lang/crates.io-index" 414 + checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" 415 + dependencies = [ 416 + "serde", 402 417 ] 403 418 404 419 [[package]] ··· 886 901 "bollard", 887 902 "color-eyre", 888 903 "docker-compose-types", 904 + "dotenv", 905 + "envy", 889 906 "itertools 0.14.0", 890 907 "luminary-macros", 891 908 "serde",
+9 -7
packages/node/Cargo.toml
··· 3 3 edition = "2024" 4 4 5 5 [dependencies] 6 - axum = "0.8.8" 6 + specta = { version = "1.0.5", features = ["functions"] } 7 7 bollard = { version = "0.20.1", features = ["chrono"] } 8 + luminary-macros = { path = "../macros" } 9 + docker-compose-types = "0.22.0" 10 + serde-saphyr = "0.0.19" 11 + serde_json = "1.0.149" 8 12 color-eyre = "0.6.5" 13 + itertools = "0.14.0" 9 14 serde = "1.0.228" 10 - serde_json = "1.0.149" 11 - specta = { version = "1.0.5", features = ["functions"] } 12 - luminary-macros = { path = "../macros" } 15 + dotenv = "0.15.0" 16 + axum = "0.8.8" 17 + envy = "0.4.2" 13 18 tokio = { version = "1.49.0", features = [ 14 19 "rt-multi-thread", 15 20 "macros", 16 21 "process", 17 22 "fs", 18 23 ] } 19 - itertools = "0.14.0" 20 - docker-compose-types = "0.22.0" 21 - serde-saphyr = "0.0.19"
+14 -1
packages/node/src/core/mod.rs
··· 3 3 use bollard::Docker; 4 4 use color_eyre::eyre::{Context, Result}; 5 5 use luminary_macros::wrap_err; 6 + use serde::Deserialize; 6 7 use tokio::process::Command; 7 8 8 9 mod model; 9 10 mod project; 10 11 12 + #[derive(Deserialize, Debug)] 13 + pub struct LuminaryConfiguration { 14 + pub project_directory: String, 15 + } 16 + 17 + #[derive(Debug)] 11 18 pub struct LuminaryCore { 19 + pub configuration: LuminaryConfiguration, 12 20 docker: Docker, 13 21 } 14 22 15 23 impl LuminaryCore { 16 24 pub fn new() -> Result<Self> { 17 25 let docker = Docker::connect_with_defaults().wrap_err("Failed to connect to docker engine.")?; 18 - return Ok(Self { docker }); 26 + let configuration = envy::prefixed("LUMINARY_").from_env::<LuminaryConfiguration>()?; 27 + 28 + return Ok(Self { 29 + configuration, 30 + docker, 31 + }); 19 32 } 20 33 21 34 #[wrap_err("Failed to read from docker compose command line interface")]
-1
packages/node/src/core/model.rs
··· 6 6 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Type)] 7 7 pub struct LuminaryProject { 8 8 pub name: String, 9 - pub dir: String, 10 9 pub services: HashMap<String, LuminaryService>, 11 10 } 12 11
+24 -15
packages/node/src/core/project.rs
··· 1 - use std::collections::HashMap; 1 + use std::{collections::HashMap, path::Path}; 2 2 3 3 use bollard::query_parameters::ListContainersOptionsBuilder; 4 4 use color_eyre::eyre::{ContextCompat, Ok, Result, WrapErr}; ··· 9 9 use crate::core::{ 10 10 LuminaryCore, 11 11 model::{LuminaryProject, LuminaryService}, 12 - project, 13 12 }; 14 13 15 14 const COMPOSE_PROJECT_DIR_LABEL: &str = "com.docker.compose.project.working_dir"; 16 15 const COMPOSE_PROJECT_LABEL: &str = "com.docker.compose.project"; 17 16 const COMPOSE_SERVICE_LABEL: &str = "com.docker.compose.service"; 18 17 19 - const ENV_PROJECT_DIRECTORY: &str = "LUMINARY_PROJECT_DIRECTORY"; 20 18 const COMPOSE_FILENAME: &str = "compose.yml"; 21 19 22 20 impl LuminaryCore { 23 21 #[wrap_err("Failed to list projects from filesystem")] 24 22 async fn list_from_filesystem(&self) -> Result<HashMap<String, LuminaryProject>> { 25 - let dir = std::env::var(ENV_PROJECT_DIRECTORY) 26 - .wrap_err("Failed to read project directory from environment variable")?; 27 - 28 23 let mut projects = HashMap::<String, LuminaryProject>::new(); 29 24 30 - let mut entries = fs::read_dir(dir) 25 + let mut entries = fs::read_dir(&self.configuration.project_directory) 31 26 .await 32 27 .wrap_err("Failed to list project directory contents")?; 33 28 while let Some(entry) = entries.next_entry().await? { ··· 43 38 if path.exists() { 44 39 let file = File::open(path).await.wrap_err("Failed to open compose file")?; 45 40 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 + ); 46 54 } 47 55 } 48 56 } ··· 65 73 && let Some(project) = labels.remove(COMPOSE_PROJECT_LABEL) 66 74 && let Some(dir) = labels.remove(COMPOSE_PROJECT_DIR_LABEL) 67 75 { 68 - acc.entry(project.clone()) 69 - .or_insert_with(|| LuminaryProject { 70 - services: HashMap::new(), 71 - name: project, 72 - dir, 73 - }) 74 - .services 75 - .insert(service.clone(), LuminaryService { name: service }); 76 + if Path::new(&dir).starts_with(&self.configuration.project_directory) { 77 + acc.entry(project.clone()) 78 + .or_insert_with(|| LuminaryProject { 79 + services: HashMap::new(), 80 + name: project, 81 + }) 82 + .services 83 + .insert(service.clone(), LuminaryService { name: service }); 84 + } 76 85 } 77 86 78 87 return acc;
+3 -2
packages/node/src/main.rs
··· 1 - use std::fs::read_dir; 2 - 3 1 use axum::Router; 4 2 use color_eyre::eyre::Result; 3 + use dotenv::dotenv; 5 4 use tokio::net::TcpListener; 6 5 7 6 mod api; ··· 9 8 10 9 #[tokio::main] 11 10 async fn main() -> Result<()> { 11 + dotenv().ok(); 12 + 12 13 let listener = TcpListener::bind("0.0.0.0:9000").await?; 13 14 let router = Router::new().nest("/api/", api::router()); 14 15