use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use cuenv_core::environment::EnvValue;
use cuenv_core::manifest::{Project, Service};
use cuenv_events::emit_stdout;
use cuenv_services::controller::{ControllerConfig, ServiceController, build_mixed_graph};
use cuenv_services::session::SessionManager;
use super::{CommandExecutor, relative_path_from_root};
pub struct UpOptions {
pub path: String,
pub package: String,
pub services: Vec<String>,
pub labels: Vec<String>,
pub environment: Option<String>,
}
pub async fn execute_up(options: &UpOptions, executor: &CommandExecutor) -> cuenv_core::Result<()> {
install_process_supervisor();
let target_path =
Path::new(&options.path)
.canonicalize()
.map_err(|e| cuenv_core::Error::Io {
source: e,
path: Some(Path::new(&options.path).to_path_buf().into_boxed_path()),
operation: "canonicalize path".to_string(),
})?;
emit_stdout!(format!(
"cuenv up: evaluating services in {} (package: {})",
options.path, options.package
));
let (filtered_services, graph, session) = {
let module = executor.get_module(&target_path)?;
let relative_path = relative_path_from_root(&module.root, &target_path);
let instance = module.get(&relative_path).ok_or_else(|| {
cuenv_core::Error::configuration(format!(
"No CUE instance found at path: {} (relative: {})",
target_path.display(),
relative_path.display()
))
})?;
let project: Project = instance.deserialize()?;
if project.services.is_empty() {
emit_stdout!("cuenv up: no services defined in configuration");
return Ok(());
}
let mut filtered_services =
filter_services(&project.services, &options.services, &options.labels);
if filtered_services.is_empty() {
emit_stdout!("cuenv up: no services match the specified filters");
return Ok(());
}
if let Some(env) = &project.env {
let project_env_vars = match options.environment.as_deref() {
Some(name) => env.for_environment(name),
None => env.base.clone(),
};
if !project_env_vars.is_empty() {
for service in filtered_services.values_mut() {
merge_project_env_into_service(&project_env_vars, &mut service.env);
}
}
}
emit_stdout!(format!(
"cuenv up: starting {} service(s): {}",
filtered_services.len(),
filtered_services
.keys()
.cloned()
.collect::<Vec<_>>()
.join(", ")
));
let graph = build_mixed_graph(&project.tasks, &filtered_services, &project.images)
.map_err(|e| {
cuenv_core::Error::execution(format!("Failed to build service graph: {e}"))
})?;
let session = SessionManager::create(&target_path, &project.name)
.map_err(|e| cuenv_core::Error::execution(format!("Failed to create session: {e}")))?;
(filtered_services, graph, session)
};
let shutdown = CancellationToken::new();
let shutdown_signal = shutdown.clone();
tokio::spawn(async move {
let _ = tokio::signal::ctrl_c().await;
shutdown_signal.cancel();
});
let controller = ServiceController::new(
ControllerConfig {
project_root: target_path,
},
shutdown,
);
controller
.execute_up(&graph, &filtered_services, Arc::new(session))
.await
.map_err(|e| cuenv_core::Error::execution(format!("Service controller failed: {e}")))
}
fn install_process_supervisor() {
#[cfg(target_os = "linux")]
{
#[expect(
unsafe_code,
reason = "PR_SET_CHILD_SUBREAPER affects only the calling process"
)]
unsafe {
libc::prctl(libc::PR_SET_CHILD_SUBREAPER, 1);
}
}
}
fn merge_project_env_into_service(
project_env: &HashMap<String, EnvValue>,
service_env: &mut HashMap<String, EnvValue>,
) {
for (key, value) in project_env {
service_env
.entry(key.clone())
.or_insert_with(|| value.clone());
}
}
fn filter_services(
all_services: &HashMap<String, Service>,
names: &[String],
labels: &[String],
) -> HashMap<String, Service> {
all_services
.iter()
.filter(|(name, service)| {
let name_match = names.is_empty() || names.contains(name);
let label_match =
labels.is_empty() || labels.iter().any(|l| service.labels.contains(l));
name_match && label_match
})
.map(|(name, service)| (name.clone(), service.clone()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_up_options_default() {
let options = UpOptions {
path: ".".to_string(),
package: "cuenv".to_string(),
services: vec![],
labels: vec![],
environment: None,
};
assert_eq!(options.path, ".");
assert_eq!(options.package, "cuenv");
assert!(options.services.is_empty());
assert!(options.labels.is_empty());
assert!(options.environment.is_none());
}
#[test]
fn test_merge_project_env_into_service_preserves_service_values() {
let mut project_env = HashMap::new();
project_env.insert("A".to_string(), EnvValue::String("project-a".to_string()));
project_env.insert("B".to_string(), EnvValue::String("project-b".to_string()));
let mut service_env = HashMap::new();
service_env.insert("B".to_string(), EnvValue::String("service-b".to_string()));
merge_project_env_into_service(&project_env, &mut service_env);
assert_eq!(
service_env.get("A"),
Some(&EnvValue::String("project-a".to_string()))
);
assert_eq!(
service_env.get("B"),
Some(&EnvValue::String("service-b".to_string()))
);
}
#[test]
fn test_filter_services_no_filters() {
let mut services = HashMap::new();
services.insert("db".to_string(), Service::default());
services.insert("api".to_string(), Service::default());
let result = filter_services(&services, &[], &[]);
assert_eq!(result.len(), 2);
}
#[test]
fn test_filter_services_by_name() {
let mut services = HashMap::new();
services.insert("db".to_string(), Service::default());
services.insert("api".to_string(), Service::default());
let result = filter_services(&services, &["db".to_string()], &[]);
assert_eq!(result.len(), 1);
assert!(result.contains_key("db"));
}
}