···88 Depot,
99 oapi::{endpoint, extract::PathParam},
1010};
1111-use serde::Deserialize;
1211use serde::Serialize;
13121413use crate::core::LuminaryProject;
1414+use crate::core::LuminaryProjectPatch;
1515use crate::{api::response::LuminaryResponse, core::LuminaryEngine, obtain};
16161717/// Returns the router for compose related endpoints.
1818pub fn router() -> Router {
1919- return Router::new().get(get_project_endpoint).patch(put_compose);
1919+ return Router::new().get(get_project_endpoint).patch(patch_compose);
2020}
21212222/// Retrieves the compose file for a given project.
···50505151/// Perform a configurable change to a project.
5252#[endpoint]
5353-pub async fn put_compose(
5353+pub async fn patch_compose(
5454 project: PathParam<String>,
5555- payload: JsonBody<LuminaryProjectPatch>,
5555+ mut payload: JsonBody<LuminaryProjectPatch>,
5656 depot: &mut Depot,
5757) -> LuminaryResponse<LuminaryProjectWithCompose> {
5858 let engine = obtain!(depot, LuminaryEngine);
5959- let mut changed = false;
60596161- if let Some(compose) = &payload.compose {
6262- engine.put_compose(&project, &compose).await?;
6363- changed = true;
6464- }
6565-6666- if let Some(from) = &payload.from {
6767- engine.rename_project(from, &project).await?;
6868- changed = true;
6969- }
7070-7171- if changed {
7272- engine.refresh().await?;
7373- }
7474-7575- return get_project(engine, &project).await;
7676-}
7777-7878-/// The payload for updating a project. Allows for multiple updates at once.
7979-#[derive(Debug, Clone, Deserialize, ToSchema)]
8080-struct LuminaryProjectPatch {
8181- /// If [Some], the new compose file for this project. If [None], the compose file will not be updated.
8282- compose: Option<String>,
8383-8484- /// If [Some], renames the project with the given name. If [None], no rename will take place.
8585- from: Option<String>,
6060+ engine.wait_until_idle(&project, None).await?;
6161+ engine.patch_project(&project, &payload.0).await?;
6262+ return get_project(engine, &payload.to.take().unwrap_or(project.0)).await;
8663}
+6-1
packages/node/src/core/action.rs
···77impl LuminaryEngine {
88 /// Updates the currently processing action for the given project and optionally, a specific service within that project.
99 #[wrap_err("Failed to set action for service")]
1010- async fn set_action(&self, project: &str, service: Option<&str>, action: LuminaryAction) -> Result<()> {
1010+ pub(super) async fn set_action(
1111+ &self,
1212+ project: &str,
1313+ service: Option<&str>,
1414+ action: LuminaryAction,
1515+ ) -> Result<()> {
1116 // Get list of targets to update
1217 let mut project_list = self.list.write().await;
1318 let targets = match project_list.0.get_mut(project) {
+1-1
packages/node/src/core/logs.rs
···102102103103 loop {
104104 match this
105105- .wait_until(&project, None, LuminaryStatus::Running)
105105+ .wait_until_status(&project, None, LuminaryStatus::Running)
106106 .await
107107 .wrap_err("Error while waiting for project to restart")
108108 {
+12-1
packages/node/src/core/model.rs
···55use bytes::{Bytes, BytesMut};
66use luminary_macros::hashmap_schema;
77use salvo::oapi::{BasicType, Components, Object, RefOr, Schema, ToSchema};
88-use serde::{Serialize, ser::SerializeStruct};
88+use serde::{Deserialize, Serialize, ser::SerializeStruct};
99use tokio::sync::{RwLock, broadcast};
10101111use crate::schema_ref_or;
···169169 Starting,
170170 Pulling,
171171 Building,
172172+ Patching,
172173}
173174174175/// Stores the log channel and buffer for a project.
···179180 // Using an Arc here to allow the worker to keep a reference to the log buffer
180181 pub buffer: Arc<RwLock<BytesMut>>,
181182}
183183+184184+/// The configuration for updating a project. Allows for multiple updates at once.
185185+#[derive(Debug, Clone, Deserialize, ToSchema)]
186186+pub struct LuminaryProjectPatch {
187187+ /// If [Some], the new compose file for this project. If [None], the compose file will not be updated.
188188+ pub compose: Option<String>,
189189+190190+ /// If [Some], renames the current to the given name. If [None], no rename will take place.
191191+ pub to: Option<String>,
192192+}
+36-12
packages/node/src/core/project.rs
···11-use std::path::{self, Path, PathBuf};
11+use std::path::{Path, PathBuf};
2233use eyre::{Context, Ok, Result};
44use futures_util::StreamExt;
55use luminary_macros::wrap_err;
66use tokio::fs::{self, read_to_string};
7788-use crate::core::{COMPOSE_FILENAME, LuminaryEngine, LuminaryStatus};
88+use crate::core::{COMPOSE_FILENAME, LuminaryAction, LuminaryEngine, LuminaryProjectPatch, LuminaryStatus};
991010impl LuminaryEngine {
1111 /// Retrieves the paths for the given project directory and its compose file.
···3030 return Ok(read_to_string(path).await.wrap_err("Failed to read file")?);
3131 }
32323333+ /// Updates the given project by applying the provided patch
3434+ pub async fn patch_project(&self, project: &str, patch: &LuminaryProjectPatch) -> Result<()> {
3535+ self.set_action(project, None, LuminaryAction::Patching).await?;
3636+3737+ let (project_path, compose_path) = self.get_path(project);
3838+ let mut changed = false;
3939+4040+ if let Some(compose) = &patch.compose {
4141+ self.put_compose(&project_path, &compose_path, &compose).await?;
4242+ changed = true;
4343+ }
4444+4545+ if let Some(to) = &patch.to {
4646+ self.rename_project(&project, &project_path, to).await?;
4747+ changed = true;
4848+ }
4949+5050+ self.set_action(project, None, LuminaryAction::Idle).await?;
5151+5252+ if changed {
5353+ self.refresh().await?;
5454+ }
5555+5656+ return Ok(());
5757+ }
5858+3359 /// Updates the docker compose file for a given project.
3460 /// WARNING: Does not automatically call `refresh`, make sure to do this after calling this function.
3561 #[wrap_err("Failed to update compose file")]
3636- pub async fn put_compose(&self, project: &str, compose: &str) -> Result<()> {
3737- let (project_path, compose_path) = self.get_path(project);
3838-6262+ async fn put_compose(&self, project_path: &PathBuf, compose_path: &PathBuf, compose: &str) -> Result<()> {
3963 // Create project directory if it doesn't exist
4064 fs::create_dir_all(&project_path)
4165 .await
···5276 /// Renames the project dirctory for a given project. Recreating the project if it was previously running.
5377 /// WARNING: Does not automatically call `refresh`, make sure to do this after calling this function.
5478 #[wrap_err("Failed to rename project")]
5555- pub async fn rename_project(&self, from: &str, to: &str) -> Result<()> {
5656- let src_path = path::absolute(self.get_path(from).0).wrap_err("Failed to get absolute path")?;
5757- let to_path = src_path.parent().unwrap().join(to);
7979+ async fn rename_project(&self, from: &str, from_path: &PathBuf, to: &str) -> Result<()> {
8080+ let to_path = from_path.parent().unwrap().join(to);
58815959- if !src_path.exists() {
8282+ if !from_path.exists() {
6083 eyre::bail!("Project '{}' does not exist", from);
6184 }
6285···7497 .any(|(_, s)| s.status != LuminaryStatus::Down);
75987699 if recreate {
7777- self.stop(&from, None).await?;
100100+ // Use manual command as action is currently "patching"
101101+ let mut stream = self.cli(&from, vec!["down", "--remove-orphans"])?;
102102+ while let Some(_) = stream.next().await {}
78103 };
791048080- fs::rename(&src_path, &to_path)
105105+ fs::rename(&from_path, &to_path)
81106 .await
82107 .wrap_err("Failed to rename project directory")?;
8310884109 if recreate {
8585- // Use manual command as the project wont be in the program state at this point
86110 let mut stream = self.cli(&to, vec!["up", "-d"])?;
87111 while let Some(_) = stream.next().await {}
88112 }
+29-2
packages/node/src/core/state.rs
···1010 secret::ContainerSummaryStateEnum,
1111};
1212use docker_compose_types::Compose;
1313-use eyre::{ContextCompat, Result, WrapErr};
1313+use eyre::{ContextCompat, Result, WrapErr, bail};
1414use futures_util::{StreamExt, stream::BoxStream};
1515use log::{debug, error, warn};
1616use luminary_macros::wrap_err;
···279279 }
280280281281 /// Waits until a given project or service reaches the desired status by listening to Docker events.
282282- pub(super) async fn wait_until(
282282+ pub(super) async fn wait_until_status(
283283 &self,
284284 project: &String,
285285 service: Option<&String>,
···311311312312 warn!("Docker event stream ended, restarting...");
313313 tokio::time::sleep(std::time::Duration::from_secs(1)).await;
314314+ }
315315+ }
316316+317317+ /// Waits until a given project or service is no longer processing any actions.
318318+ pub async fn wait_until_idle(&self, project: &String, service: Option<&String>) -> Result<()> {
319319+ let mut stream = self.state_subscribe().await;
320320+321321+ loop {
322322+ if let Some(list) = stream.next().await {
323323+ if let Some(busy) = list.0.get(project).and_then(|p| {
324324+ if service.is_none() {
325325+ return Some(p.busy());
326326+ } else {
327327+ return p
328328+ .services
329329+ .0
330330+ .get(service?)
331331+ .map(|s| s.action != LuminaryAction::Idle);
332332+ }
333333+ }) {
334334+ if !busy {
335335+ return Ok(());
336336+ }
337337+ } else {
338338+ bail!("Project or service does not exist");
339339+ }
340340+ }
314341 }
315342 }
316343}