use crate::api::{RunnerResources, RunnerToProvision};
#[cfg(target_os = "macos")]
use crate::api::TemplateConfig;
#[cfg(target_os = "macos")]
use crate::lume::{
check_template_exists, create_template, find_matching_template, generate_template_name,
};
use log::{error, info};
use std::sync::Arc;
use tokio::sync::Semaphore;
pub enum ProvisionOutcome {
Success,
Failed {
error: String,
diagnostics: serde_json::Map<String, serde_json::Value>,
},
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
HostFull {
code: String,
message: String,
retry_after_secs: u64,
},
}
impl ProvisionOutcome {
pub fn failed(msg: impl Into<String>) -> Self {
Self::Failed {
error: msg.into(),
diagnostics: serde_json::Map::new(),
}
}
}
pub struct ProvisionResult {
pub runner_name: String,
pub executor_kind: Option<crate::executor::ExecutorKind>,
pub outcome: ProvisionOutcome,
}
pub async fn provision_single_runner(
runner: RunnerToProvision,
semaphore: Arc<Semaphore>,
registry: Arc<crate::executor::registry::Registry>,
) -> ProvisionResult {
let _permit = semaphore.acquire().await.expect("semaphore closed");
info!(
"Processing runner: {} (image: {}, os: {}, cpu: {}, mem: {}GB, disk: {}GB)",
runner.name, runner.image, runner.os, runner.cpu, runner.memory, runner.disk
);
#[cfg(target_os = "macos")]
let template_config = {
let (image_registry, image) = if runner.image.contains('.')
&& runner.image.split('/').next().unwrap().contains('.')
{
let parts: Vec<&str> = runner.image.splitn(2, '/').collect();
if parts.len() == 2 {
(Some(parts[0].to_string()), parts[1].to_string())
} else {
(Some("ghcr.io".to_string()), runner.image.clone())
}
} else {
(Some("ghcr.io".to_string()), runner.image.clone())
};
TemplateConfig {
image,
registry: image_registry,
organization: None,
cpu: runner.cpu,
memory: runner.memory,
disk: runner.disk,
os: runner.os.clone(),
}
};
let executor_kind = match crate::executor::resolve_executor_kind(
runner.executor.as_deref(),
runner.extra_config.as_ref(),
&runner.os,
) {
Ok(k) => k,
Err(e) => {
return ProvisionResult {
runner_name: runner.name.clone(),
executor_kind: None,
outcome: ProvisionOutcome::failed(format!("Cannot derive executor: {e}")),
};
}
};
let template_name = if matches!(
executor_kind,
crate::executor::ExecutorKind::Docker | crate::executor::ExecutorKind::Meda
) {
info!(
"Using {:?} executor - using image name directly: {}",
executor_kind, runner.image
);
Some(runner.image.clone())
} else {
#[cfg(target_os = "macos")]
{
if let Some(existing_template) = find_matching_template(&template_config).await {
info!(
"Found existing template with matching configuration: {}",
existing_template
);
Some(existing_template)
} else {
let generated_name = generate_template_name(&template_config);
let template_exists = check_template_exists(&generated_name).await;
if !template_exists {
info!(
"No matching template found. Creating new template '{}' from image '{}'",
generated_name, template_config.image
);
match create_template(&template_config, &generated_name).await {
Ok(_) => {
info!("Successfully created template: {}", generated_name);
Some(generated_name)
}
Err(e) => {
error!("Failed to create template {}: {}", generated_name, e);
return ProvisionResult {
runner_name: runner.name.clone(),
executor_kind: Some(executor_kind),
outcome: ProvisionOutcome::failed(format!(
"Template creation failed: {}",
e
)),
};
}
}
} else {
info!("Using existing template: {}", generated_name);
Some(generated_name)
}
}
}
#[cfg(not(target_os = "macos"))]
{
None
}
};
let template_name = match template_name {
Some(t) => t,
None => {
return ProvisionResult {
runner_name: runner.name.clone(),
executor_kind: Some(executor_kind),
outcome: ProvisionOutcome::failed("No template available".to_string()),
};
}
};
info!(
"Provisioning runner '{}' with template '{}'",
runner.name, template_name
);
let resources = RunnerResources {
cpu: runner.cpu,
memory: runner.memory,
disk: runner.disk,
};
let gpu_raw = runner
.gpu
.as_ref()
.map(|s| serde_json::Value::String(s.clone()))
.or_else(|| {
runner
.extra_config
.as_ref()
.and_then(|c| c.get("gpu").cloned())
});
let gpu = match crate::executor::parse_gpu_request(gpu_raw.as_ref()) {
Ok(g) => g,
Err(e) => {
return ProvisionResult {
runner_name: runner.name.clone(),
executor_kind: Some(executor_kind),
outcome: ProvisionOutcome::failed(format!("Invalid gpu request: {e}")),
};
}
};
let docker_privileged = runner
.extra_config
.as_ref()
.and_then(|v| v.get("privileged"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
let docker_mount_socket = runner
.extra_config
.as_ref()
.and_then(|v| v.get("docker_socket_mount"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
let spec = crate::executor::RunnerSpec {
name: runner.name.clone(),
provision_script: runner.provision_script.clone(),
image: template_name.clone(),
cpu: resources.cpu,
memory_gb: resources.memory,
disk_gb: resources.disk,
gpu,
docker_privileged,
docker_mount_socket,
login: crate::executor::RunnerLogin {
username: runner.login.username.clone(),
password: runner.login.password.clone(),
},
};
let outcome = match registry.get(executor_kind) {
Ok(exec) => match exec.provision(&spec).await {
Ok(()) => {
info!(
"Successfully provisioned runner: {} using template {}",
runner.name, template_name
);
ProvisionOutcome::Success
}
Err(crate::executor::ProvisionError::HostFull {
code,
message,
retry_after_secs,
}) => {
info!(
"Host at capacity for runner {}: {} ({}). Retry-After {}s",
runner.name, message, code, retry_after_secs
);
ProvisionOutcome::HostFull {
code,
message,
retry_after_secs,
}
}
Err(e) => {
let error_msg = e.to_string();
error!(
"Failed to provision runner {} using template {}: {}",
runner.name, template_name, error_msg
);
ProvisionOutcome::failed(error_msg)
}
},
Err(e) => {
let error_msg = e.to_string();
error!(
"Failed to provision runner {} (registry lookup): {}",
runner.name, error_msg
);
ProvisionOutcome::failed(error_msg)
}
};
ProvisionResult {
runner_name: runner.name.clone(),
executor_kind: Some(executor_kind),
outcome,
}
}