use super::OrchestrateError;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct BundleResult {
pub bundle_path: PathBuf,
pub workspace_path: PathBuf,
pub bundle_id: String,
}
pub fn build_bundle(
pack_path: &Path,
bundle_name: &str,
providers: Option<&[String]>,
) -> Result<BundleResult, OrchestrateError> {
if !pack_path.exists() {
return Err(OrchestrateError::Cards2Pack(format!(
"pack file not found: {}",
pack_path.display()
)));
}
let tmp = tempfile::tempdir()?;
let workspace = tmp.path().to_path_buf();
let bundle_id = sanitize(bundle_name);
let pack_filename = pack_path
.file_name()
.map(|f| f.to_string_lossy().to_string())
.unwrap_or_else(|| format!("{bundle_id}.gtpack"));
let local_pack = workspace.join(&pack_filename);
std::fs::copy(pack_path, &local_pack)?;
let provider_refs: Vec<String> = match providers {
Some(provs) if !provs.is_empty() => provs.to_vec(),
_ => vec!["oci://ghcr.io/greenticai/packs/state/state-memory:latest".to_string()],
};
let provider_entries: Vec<String> = provider_refs
.iter()
.map(|r| {
let id = r
.rsplit('/')
.next()
.unwrap_or("provider")
.split(':')
.next()
.unwrap_or("provider");
format!(
r#" {{
"reference": "{r}",
"detected_kind": "oci",
"provider_id": "{id}",
"display_name": "{id}"
}}"#
)
})
.collect();
let pack_ref = format!("file://./{pack_filename}");
let answers_json = format!(
r#"{{
"wizard_id": "greentic-bundle.wizard.run",
"schema_id": "greentic-bundle.wizard.answers",
"schema_version": "1.0.0",
"locale": "en",
"answers": {{
"bundle_id": "{bundle_id}",
"bundle_name": "{bundle_name}",
"advanced_setup": false,
"app_packs": ["{pack_ref}"],
"app_pack_entries": [
{{
"reference": "{pack_ref}",
"detected_kind": "file",
"display_name": "{bundle_name}",
"pack_id": "{bundle_id}",
"mapping": {{ "scope": "global" }}
}}
],
"extension_providers": [{provider_refs_json}],
"extension_provider_entries": [
{provider_entries_joined}
],
"access_rules": [
{{
"rule_path": "{bundle_id}",
"policy": "allow",
"tenant": "default"
}}
],
"setup_execution_intent": false,
"export_intent": false
}}
}}"#,
bundle_id = bundle_id,
bundle_name = bundle_name,
pack_ref = pack_ref,
provider_refs_json = provider_refs
.iter()
.map(|r| format!("\"{r}\""))
.collect::<Vec<_>>()
.join(", "),
provider_entries_joined = provider_entries.join(",\n"),
);
let answers_path = workspace.join("answers.json");
std::fs::write(&answers_path, &answers_json)?;
let wizard_output = std::process::Command::new("greentic-bundle")
.args([
"wizard",
"apply",
"--answers",
&answers_path.display().to_string(),
])
.current_dir(&workspace)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()?;
let bundle_workspace = workspace.join(format!("{bundle_id}-bundle"));
if !wizard_output.status.success() {
let stderr = String::from_utf8_lossy(&wizard_output.stderr);
let stdout = String::from_utf8_lossy(&wizard_output.stdout);
if !bundle_workspace.join("bundle.yaml").exists() {
return Err(OrchestrateError::Cards2Pack(format!(
"greentic-bundle wizard apply failed (exit {}):\n{}\n{}",
wizard_output.status.code().unwrap_or(-1),
stderr.trim(),
stdout.trim()
)));
}
}
let build_output = std::process::Command::new("greentic-bundle")
.args(["build", "--root", &bundle_workspace.display().to_string()])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()?;
if !build_output.status.success() {
let stderr = String::from_utf8_lossy(&build_output.stderr);
let stdout = String::from_utf8_lossy(&build_output.stdout);
return Err(OrchestrateError::Cards2Pack(format!(
"greentic-bundle build failed (exit {}):\n{}\n{}",
build_output.status.code().unwrap_or(-1),
stderr.trim(),
stdout.trim()
)));
}
if let Some(bundle_path) = find_gtbundle(&bundle_workspace) {
let _ = tmp.keep();
return Ok(BundleResult {
bundle_path,
workspace_path: bundle_workspace,
bundle_id,
});
}
if let Some(bundle_path) = find_gtbundle(&workspace) {
let _ = tmp.keep();
return Ok(BundleResult {
bundle_path,
workspace_path: bundle_workspace,
bundle_id,
});
}
Err(OrchestrateError::Cards2Pack(
"greentic-bundle build succeeded but no .gtbundle file found".to_string(),
))
}
fn find_gtbundle(dir: &Path) -> Option<PathBuf> {
if !dir.exists() {
return None;
}
find_file_recursive(dir, "gtbundle", 3)
}
fn find_file_recursive(dir: &Path, ext: &str, depth: u8) -> Option<PathBuf> {
if depth == 0 {
return None;
}
for entry in std::fs::read_dir(dir).ok()?.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == ext) {
return Some(path);
}
if let Some(found) = path
.is_dir()
.then(|| find_file_recursive(&path, ext, depth - 1))
.flatten()
{
return Some(found);
}
}
None
}
fn sanitize(s: &str) -> String {
s.chars()
.map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' {
c
} else {
'-'
}
})
.collect()
}
#[derive(Debug, Clone)]
pub struct DeployResult {
pub deploy_url: String,
pub spec_path: PathBuf,
}
pub fn target_requirements(cloud: &str) -> Result<serde_json::Value, OrchestrateError> {
let output = std::process::Command::new("greentic-deployer")
.args(["target-requirements", "--provider", cloud])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(OrchestrateError::Deployer(format!(
"greentic-deployer target-requirements failed: {stderr}"
)));
}
let json: serde_json::Value = serde_json::from_slice(&output.stdout)
.map_err(|e| OrchestrateError::Deployer(format!("invalid JSON from deployer: {e}")))?;
Ok(json)
}
pub fn deploy_bundle(
bundle_path: &Path,
name: &str,
_cloud: &str,
) -> Result<DeployResult, OrchestrateError> {
let tmp = tempfile::tempdir()?;
let spec_path = tmp.path().join(format!("{name}-spec.yaml"));
let render = std::process::Command::new("greentic-deployer")
.args([
"single-vm",
"render-spec",
"--out",
&spec_path.display().to_string(),
"--name",
name,
"--bundle-source",
&format!("file://{}", bundle_path.display()),
])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()?;
if !render.status.success() {
let stderr = String::from_utf8_lossy(&render.stderr);
return Err(OrchestrateError::Deployer(format!(
"greentic-deployer render-spec failed: {stderr}"
)));
}
let apply = std::process::Command::new("greentic-deployer")
.args([
"single-vm",
"apply",
"--spec",
&spec_path.display().to_string(),
])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()?;
if !apply.status.success() {
let stderr = String::from_utf8_lossy(&apply.stderr);
return Err(OrchestrateError::Deployer(format!(
"greentic-deployer apply failed: {stderr}"
)));
}
let status = std::process::Command::new("greentic-deployer")
.args([
"single-vm",
"status",
"--spec",
&spec_path.display().to_string(),
])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()?;
let deploy_url = if status.status.success() {
let json: serde_json::Value =
serde_json::from_slice(&status.stdout).unwrap_or_else(|_| serde_json::json!({}));
json.get("url")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string()
} else {
"deployment-pending".to_string()
};
let _ = tmp.keep();
Ok(DeployResult {
deploy_url,
spec_path,
})
}