mirror of
https://codeberg.org/icewind/haze.git
synced 2026-10-01 08:44:09 +02:00
feat(start): Add --detach flag
This will create a dedicated worktree for the instance. App listed to be enabled will also benefit from their own worktrees. The worktree is cleanup when the instance is destroyed.
This commit is contained in:
parent
c54302fdaa
commit
0e702f7b87
9 changed files with 339 additions and 27 deletions
156
src/cloud.rs
156
src/cloud.rs
|
|
@ -1,6 +1,7 @@
|
|||
use crate::config::{HazeConfig, HazeVolumeConfig};
|
||||
use crate::database::Database;
|
||||
use crate::exec::{exec, exec_io, exec_tty, ExitCode};
|
||||
use crate::git::{create_worktree, find_app_repo, init_submodules, remove_worktree};
|
||||
use crate::mapping::{for_config, Mapping};
|
||||
use crate::php::PhpVersion;
|
||||
use crate::service::Service;
|
||||
|
|
@ -53,6 +54,116 @@ fn get_max_php_version(base_path: &Utf8Path) -> Option<PhpVersion> {
|
|||
|
||||
const PINNED_FILE: &str = "/var/www/html/data/.haze-pinned";
|
||||
|
||||
fn enabled_apps(config: &HazeConfig, options: &CloudOptions) -> Vec<String> {
|
||||
let mut apps: Vec<String> = config.auto_setup.enable_apps.clone();
|
||||
for service in &options.services {
|
||||
apps.extend(service.apps().iter().map(|app| app.to_string()));
|
||||
|
||||
// Presets do not list apps in apps(), so we need to fetch them from the config.
|
||||
for preset in config.preset.iter().filter(|p| p.name == service.name()) {
|
||||
apps.extend(preset.apps.iter().cloned());
|
||||
}
|
||||
}
|
||||
apps.dedup();
|
||||
apps
|
||||
}
|
||||
|
||||
async fn creates_worktrees_for_instance(
|
||||
config: &HazeConfig,
|
||||
id: &str,
|
||||
options: &CloudOptions,
|
||||
git_ref: &str,
|
||||
) -> Result<(Utf8PathBuf, Vec<HazeVolumeConfig>)> {
|
||||
let worktree_dir = config.worktree_dir.join(id);
|
||||
|
||||
create_dir_all(&worktree_dir)
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to create directory for worktrees")?;
|
||||
|
||||
println!("Creating worktree of {} at {}", git_ref, worktree_dir);
|
||||
create_worktree(&config.sources_root, &worktree_dir, git_ref)?;
|
||||
init_submodules(&worktree_dir).wrap_err("Failed to setup submodules")?;
|
||||
|
||||
for app in enabled_apps(config, options) {
|
||||
let Some(app_repo) = find_app_repo(config, &app) else {
|
||||
continue;
|
||||
};
|
||||
let Some(dir_name) = app_repo.parent().and_then(|dir| dir.file_name()) else {
|
||||
continue;
|
||||
};
|
||||
let app_worktree = worktree_dir.join(dir_name).join(&app);
|
||||
|
||||
println!("Creating worktree of {} at {}", app, app_worktree);
|
||||
create_worktree(&app_repo, &app_worktree, "HEAD")
|
||||
.wrap_err_with(|| format!("Failed to create worktree for {app}"))?;
|
||||
}
|
||||
|
||||
Ok((worktree_dir, worktree_volumes(config, id)))
|
||||
}
|
||||
|
||||
async fn remove_worktrees_for_instance(config: &HazeConfig, id: &str) -> Result<()> {
|
||||
let worktree_dir = config.worktree_dir.join(id);
|
||||
|
||||
remove_worktree(&worktree_dir)?;
|
||||
|
||||
for app_dir in &config.app_directories {
|
||||
let Some(dir_name) = app_dir.file_name() else {
|
||||
continue;
|
||||
};
|
||||
let Ok(app_worktrees) = worktree_dir.join(dir_name).read_dir_utf8() else {
|
||||
continue;
|
||||
};
|
||||
for app_worktree in app_worktrees.flatten() {
|
||||
remove_worktree(app_worktree.path())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn worktree_volumes(config: &HazeConfig, id: &str) -> Vec<HazeVolumeConfig> {
|
||||
// In order to be able to run git operations on the worktree within the container,
|
||||
// we need to mount the original repository too.
|
||||
fn repo_volume(repo: &Utf8Path) -> HazeVolumeConfig {
|
||||
HazeVolumeConfig {
|
||||
create: false,
|
||||
source: repo.join(".git"),
|
||||
read_only: false,
|
||||
target: repo.join(".git"),
|
||||
}
|
||||
}
|
||||
|
||||
let worktree_dir = config.worktree_dir.join(id);
|
||||
|
||||
let mut volumes = vec![repo_volume(&config.sources_root)];
|
||||
|
||||
for app_dir in &config.app_directories {
|
||||
let Some(dir_name) = app_dir.file_name() else {
|
||||
continue;
|
||||
};
|
||||
let Ok(app_worktrees) = worktree_dir.join(dir_name).read_dir_utf8() else {
|
||||
continue;
|
||||
};
|
||||
for app_worktree in app_worktrees.flatten() {
|
||||
let app = app_worktree.file_name().to_string();
|
||||
let Some(app_repo) = find_app_repo(config, &app) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
volumes.push(HazeVolumeConfig {
|
||||
create: false,
|
||||
source: app_worktree.into_path(),
|
||||
read_only: false,
|
||||
target: format!("/var/www/html/{dir_name}/{app}").into(),
|
||||
});
|
||||
volumes.push(repo_volume(&app_repo));
|
||||
}
|
||||
}
|
||||
|
||||
volumes
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Default)]
|
||||
pub struct CloudOptions {
|
||||
pub name: Option<String>,
|
||||
|
|
@ -61,6 +172,7 @@ pub struct CloudOptions {
|
|||
pub services: Vec<Service>,
|
||||
pub app_packages: Vec<Utf8PathBuf>,
|
||||
pub version: Option<String>,
|
||||
pub detach: bool,
|
||||
}
|
||||
|
||||
impl CloudOptions {
|
||||
|
|
@ -74,6 +186,7 @@ impl CloudOptions {
|
|||
services: vec![],
|
||||
app_packages: vec![],
|
||||
version: None,
|
||||
detach: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -88,6 +201,7 @@ impl CloudOptions {
|
|||
let mut services = Vec::new();
|
||||
let mut app_package = Vec::new();
|
||||
let mut version = None;
|
||||
let mut detach = false;
|
||||
|
||||
while let Some(option) = args.peek() {
|
||||
if let Ok(db_option) = Database::from_str(option.as_ref()) {
|
||||
|
|
@ -108,6 +222,9 @@ impl CloudOptions {
|
|||
} else if option.as_ref() == "--name" {
|
||||
let _ = args.next();
|
||||
name = args.next().map(|s| s.into());
|
||||
} else if option.as_ref() == "--detach" {
|
||||
let _ = args.next();
|
||||
detach = true;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
|
@ -122,6 +239,7 @@ impl CloudOptions {
|
|||
services,
|
||||
app_packages: app_package,
|
||||
version,
|
||||
detach,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -254,10 +372,10 @@ impl Cloud {
|
|||
.wrap_err("Failed to create directory for app packages")?;
|
||||
}
|
||||
|
||||
let source_root = if let Some(version) = options.version.as_deref() {
|
||||
download_nc(config, version).await?
|
||||
} else {
|
||||
config.sources_root.clone()
|
||||
let (source_root, source_volumes) = match (options.version.as_deref(), options.detach) {
|
||||
(Some(version), _) => (download_nc(config, version).await?, Vec::new()),
|
||||
(None, true) => creates_worktrees_for_instance(config, &id, &options, "HEAD").await?,
|
||||
(None, false) => (config.sources_root.clone(), Vec::new()),
|
||||
};
|
||||
|
||||
let app_volumes = options
|
||||
|
|
@ -289,11 +407,12 @@ impl Cloud {
|
|||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let mappings = for_config(config)
|
||||
.chain(source_volumes.iter().map(Mapping::from))
|
||||
.chain(app_volumes.iter().map(Mapping::from))
|
||||
.collect::<Vec<_>>();
|
||||
for mapping in &mappings {
|
||||
mapping
|
||||
.create(&id, config)
|
||||
.create(&id, config, &source_root)
|
||||
.await
|
||||
.wrap_err_with(|| format!("Failed to setup work directory {}", mapping.source))?;
|
||||
}
|
||||
|
|
@ -561,7 +680,7 @@ impl Cloud {
|
|||
})
|
||||
}
|
||||
|
||||
pub async fn destroy(self, docker: &Docker) -> Result<()> {
|
||||
pub async fn destroy(self, docker: &Docker, config: &HazeConfig) -> Result<()> {
|
||||
for container in self.containers {
|
||||
let _ = docker
|
||||
.kill_container(container.trim_start_matches('/'), None)
|
||||
|
|
@ -593,6 +712,8 @@ impl Cloud {
|
|||
}
|
||||
}
|
||||
|
||||
remove_worktrees_for_instance(config, &self.id).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -701,6 +822,15 @@ impl Cloud {
|
|||
let db = labels.get("haze-db")?.parse().ok()?;
|
||||
let php = labels.get("haze-php")?.parse().ok()?;
|
||||
let version = labels.get("haze-version").cloned();
|
||||
let sources_root = cloud
|
||||
.mounts
|
||||
.as_ref()?
|
||||
.iter()
|
||||
.find(|mount| mount.destination.as_deref() == Some("/var/www/html"))?
|
||||
.source
|
||||
.as_deref();
|
||||
|
||||
let detach = sources_root.map(Utf8Path::new)? != config.sources_root;
|
||||
|
||||
let found_services = labels
|
||||
.get("haze-services")?
|
||||
|
|
@ -740,6 +870,7 @@ impl Cloud {
|
|||
services: found_services,
|
||||
app_packages: vec![],
|
||||
version,
|
||||
detach,
|
||||
},
|
||||
address,
|
||||
preset_config: HashMap::default(),
|
||||
|
|
@ -847,15 +978,24 @@ impl Cloud {
|
|||
format!("/var/www/html/{path}").into()
|
||||
};
|
||||
|
||||
let mut mappings = for_config(config).collect::<Vec<_>>();
|
||||
let worktree_volumes = worktree_volumes(config, &self.id);
|
||||
let mut mappings = for_config(config)
|
||||
.chain(worktree_volumes.iter().map(Mapping::from))
|
||||
.collect::<Vec<_>>();
|
||||
mappings.sort_by_key(|mapping| usize::MAX - mapping.target.as_str().len());
|
||||
|
||||
let source_root = if self.options.detach {
|
||||
config.worktree_dir.join(&self.id)
|
||||
} else {
|
||||
config.sources_root.clone()
|
||||
};
|
||||
|
||||
for mapping in mappings {
|
||||
if let Some(rel_path) = path.strip_prefix(mapping.target.as_str()) {
|
||||
let rel_path = rel_path.trim_matches('/');
|
||||
return Some(
|
||||
mapping
|
||||
.source(&self.id, config, &config.sources_root)
|
||||
.source(&self.id, config, &source_root)
|
||||
.join(rel_path),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue