···99 let func = parse_macro_input!(item as ItemFn);
1010 let attr: TokenStream2 = attr.into();
1111 let visibility = &func.vis;
1212+ let attrs = &func.attrs;
12131314 // Create variables for quoting
1415 let asyncness = &func.sig.asyncness;
···2223 };
23242425 return quote! {
2626+ #(#attrs)*
2527 #visibility #signature {
2628 color_eyre::eyre::WrapErr::wrap_err((#asyncness move || #output #block)()#wait, #attr)
2729 }
+3
packages/node/src/core/mod.rs
···99mod model;
1010mod project;
11111212+/// A struct containing Luminary configuration, to be loaded from environment variables.
1213#[derive(Deserialize, Debug)]
1314pub struct LuminaryConfiguration {
1415 pub project_directory: String,
1516}
16171818+/// The core struct of the Luminary application, containing shared state and configuration.
1719#[derive(Debug)]
1820pub struct LuminaryCore {
1921 pub configuration: LuminaryConfiguration,
···2123}
22242325impl LuminaryCore {
2626+ /// Initializes a new instance of the LuminaryCore struct, loading configuration from environment variables and connecting to the Docker engine.
2427 pub fn new() -> Result<Self> {
2528 let docker = Docker::connect_with_defaults().wrap_err("Failed to connect to docker engine.")?;
2629 let configuration = envy::prefixed("LUMINARY_").from_env::<LuminaryConfiguration>()?;
+30
packages/node/src/core/model.rs
···11+//! This module defines the core data models used within the Luminary application.
22+13use std::collections::HashMap;
2435use serde::{Deserialize, Serialize};
46use specta::Type;
5788+/// Represents a Luminary project, consisting of a docker compose project.
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Type)]
1010+#[serde(rename_all = "camelCase")]
711pub struct LuminaryProject {
812 pub name: String,
1313+ pub status: LuminaryStatus,
914 pub services: HashMap<String, LuminaryService>,
1015}
11161717+/// Represents a service within a Luminary project.
1218#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Type)]
1919+#[serde(rename_all = "camelCase")]
1320pub struct LuminaryService {
1421 pub name: String,
2222+ pub status: LuminaryStatus,
2323+}
2424+2525+/// Represents the various possible activity statuses of a Luminary service.
2626+/// Variants are ordered from lowest (Exited) to highest (Healthy).
2727+#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Type)]
2828+#[serde(rename_all = "camelCase")]
2929+pub enum LuminaryStatus {
3030+ Exited,
3131+ Down,
3232+ Paused,
3333+ Restarting,
3434+ Removing,
3535+ Starting,
3636+ Running,
3737+ Healthy,
3838+}
3939+4040+impl LuminaryStatus {
4141+ /// Returns the lowest status from an iterator, or `None` if empty.
4242+ pub fn min(statuses: impl IntoIterator<Item = Self>) -> Self {
4343+ statuses.into_iter().min().unwrap_or(LuminaryStatus::Down)
4444+ }
1545}
+71-24
packages/node/src/core/project.rs
···11+//! This module implements the core logic for managing Luminary projects.
22+13use std::{collections::HashMap, path::Path};
2433-use bollard::query_parameters::ListContainersOptionsBuilder;
44-use color_eyre::eyre::{ContextCompat, Ok, Result, WrapErr};
55+use bollard::{query_parameters::ListContainersOptionsBuilder, secret::ContainerSummaryStateEnum};
66+use color_eyre::eyre::{Ok, Result, WrapErr};
57use docker_compose_types::Compose;
68use luminary_macros::wrap_err;
79use tokio::fs::{self, File};
810911use crate::core::{
1012 LuminaryCore,
1111- model::{LuminaryProject, LuminaryService},
1313+ model::{LuminaryProject, LuminaryService, LuminaryStatus},
1214};
13151416const COMPOSE_PROJECT_DIR_LABEL: &str = "com.docker.compose.project.working_dir";
···1820const COMPOSE_FILENAME: &str = "compose.yml";
19212022impl LuminaryCore {
2323+ /// Lists all Luminary projects by combining data from both the filesystem and Docker engine.
2424+ #[wrap_err("Failed to list projects")]
2525+ pub async fn list_projects(&self) -> Result<HashMap<String, LuminaryProject>> {
2626+ let disk_projects = self.list_from_filesystem().await?;
2727+ let mut projects = self.list_from_docker().await?;
2828+2929+ for (name, disk) in disk_projects {
3030+ let project = projects.entry(name).or_insert(disk);
3131+ project.status = LuminaryStatus::min(project.services.values().map(|s| s.status));
3232+ }
3333+3434+ return Ok(projects);
3535+ }
3636+3737+ /// Lists all Luminary projects found in the configured projects directory.
2138 #[wrap_err("Failed to list projects from filesystem")]
2239 async fn list_from_filesystem(&self) -> Result<HashMap<String, LuminaryProject>> {
2340 let mut projects = HashMap::<String, LuminaryProject>::new();
···2744 .wrap_err("Failed to list project directory contents")?;
2845 while let Some(entry) = entries.next_entry().await? {
2946 let mut path = entry.path();
3030- if path.is_dir() {
3131- let project_name = path
3232- .file_name()
3333- .wrap_err("Failed to get project directory name")?
3434- .to_str()
3535- .wrap_err("Failed to read project directory name")?
3636- .to_owned();
4747+ if path.is_dir()
4848+ && let Some(project_name) = path.file_name().and_then(|n| n.to_str()).map(|s| s.to_string())
4949+ {
3750 path.push(COMPOSE_FILENAME);
3851 if path.exists() {
3952 let file = File::open(path).await.wrap_err("Failed to open compose file")?;
4053 let compose: Compose = serde_saphyr::from_reader(file.into_std().await)?;
4141-4242- projects.insert(
4343- project_name.clone(),
4444- LuminaryProject {
4545- name: project_name,
4646- services: compose
4747- .services
4848- .0
4949- .into_iter()
5050- .map(|(name, _)| (name.clone(), LuminaryService { name: name.clone() }))
5151- .collect(),
5252- },
5353- );
5454+ projects.insert(project_name.clone(), self.parse_compose(project_name, compose));
5455 }
5556 }
5657 }
···5859 return Ok(projects);
5960 }
60616262+ /// Parses a given compose file into a LuminaryProject struct, initializing all services with a default status of "Down".
6363+ fn parse_compose(&self, name: String, compose: Compose) -> LuminaryProject {
6464+ LuminaryProject {
6565+ name,
6666+ status: LuminaryStatus::Down,
6767+ services: compose
6868+ .services
6969+ .0
7070+ .into_iter()
7171+ .map(|(name, _)| {
7272+ (
7373+ name.clone(),
7474+ LuminaryService {
7575+ name: name.clone(),
7676+ status: LuminaryStatus::Down,
7777+ },
7878+ )
7979+ })
8080+ .collect(),
8181+ }
8282+ }
8383+8484+ /// Lists all Luminary projects by querying the Docker engine for containers with specific labels.
6185 #[wrap_err("Failed to list projects from docker engine")]
6286 async fn list_from_docker(&self) -> Result<HashMap<String, LuminaryProject>> {
6387 let options = ListContainersOptionsBuilder::default().all(true).build();
···7498 && let Some(dir) = labels.remove(COMPOSE_PROJECT_DIR_LABEL)
7599 {
76100 if Path::new(&dir).starts_with(&self.configuration.project_directory) {
101101+ let status = self.parse_state(container.state);
77102 acc.entry(project.clone())
78103 .or_insert_with(|| LuminaryProject {
79104 services: HashMap::new(),
105105+ status: status,
80106 name: project,
81107 })
82108 .services
8383- .insert(service.clone(), LuminaryService { name: service });
109109+ .insert(
110110+ service.clone(),
111111+ LuminaryService {
112112+ name: service,
113113+ status,
114114+ },
115115+ );
84116 }
85117 }
86118···88120 });
8912190122 return Ok(projects);
123123+ }
124124+125125+ /// Parses a Docker container state into a corresponding LuminaryStatus.
126126+ fn parse_state(&self, state: Option<ContainerSummaryStateEnum>) -> LuminaryStatus {
127127+ return match state {
128128+ Some(ContainerSummaryStateEnum::CREATED) => LuminaryStatus::Starting,
129129+ Some(ContainerSummaryStateEnum::RUNNING) => LuminaryStatus::Running,
130130+ Some(ContainerSummaryStateEnum::PAUSED) => LuminaryStatus::Paused,
131131+ Some(ContainerSummaryStateEnum::RESTARTING) => LuminaryStatus::Restarting,
132132+ Some(ContainerSummaryStateEnum::EXITED) => LuminaryStatus::Exited,
133133+ Some(ContainerSummaryStateEnum::REMOVING) => LuminaryStatus::Removing,
134134+ Some(ContainerSummaryStateEnum::EMPTY) => LuminaryStatus::Down,
135135+ Some(ContainerSummaryStateEnum::DEAD) => LuminaryStatus::Down,
136136+ None => LuminaryStatus::Down,
137137+ };
91138 }
92139}
+8-4
packages/node/src/main.rs
···1010async fn main() -> Result<()> {
1111 dotenv().ok();
12121313- let listener = TcpListener::bind("0.0.0.0:9000").await?;
1414- let router = Router::new().nest("/api/", api::router());
1313+ let core = core::LuminaryCore::new()?;
1414+ let projects = core.list_projects().await?;
1515+ println!("Projects: {:#?}", projects);
15161616- println!("Listening on http://127.0.0.1:{}", listener.local_addr()?.port());
1717- axum::serve(listener, router).await?;
1717+ // let listener = TcpListener::bind("0.0.0.0:9000").await?;
1818+ // let router = Router::new().nest("/api/", api::router());
1919+2020+ // println!("Listening on http://127.0.0.1:{}", listener.local_addr()?.port());
2121+ // axum::serve(listener, router).await?;
1822 return Ok(());
1923}