// Parmanode script runner - executes Parmanode installation scripts in containers // Provides compatibility layer for existing Parmanode modules use archipelago_container::{PodmanClient, AppManifest}; use anyhow::{Context, Result}; use std::path::PathBuf; use std::process::Command; use tokio::fs; use tracing::{info, warn}; pub struct ParmanodeScriptRunner { podman: PodmanClient, scripts_dir: PathBuf, } impl ParmanodeScriptRunner { pub fn new(scripts_dir: PathBuf) -> Self { Self { podman: PodmanClient::new("archipelago".to_string()), scripts_dir, } } /// Detect if a path contains a Parmanode script pub fn is_parmanode_script(&self, path: &PathBuf) -> bool { // Check for common Parmanode script patterns path.file_name() .and_then(|name| name.to_str()) .map(|name| { name.ends_with(".sh") && ( name.contains("parmanode") || name.contains("bitcoin") || name.contains("lightning") || name.contains("electrs") ) }) .unwrap_or(false) } /// Run a Parmanode script in an isolated container pub async fn run_script(&self, script_path: &PathBuf) -> Result<()> { info!("Running Parmanode script: {:?}", script_path); // Create a temporary container manifest for the script let script_name = script_path .file_stem() .and_then(|s| s.to_str()) .unwrap_or("parmanode-script"); // Create a minimal container to run the script let container_name = format!("parmanode-{}", script_name); // Copy script to a location accessible by containers let script_content = fs::read_to_string(script_path).await .context("Failed to read Parmanode script")?; // Create a wrapper script that runs in Alpine let wrapper_script = format!( r#"#!/bin/sh set -e {} "#, script_content ); // Write wrapper to temp location let temp_script = format!("/tmp/parmanode-{}.sh", script_name); fs::write(&temp_script, wrapper_script).await .context("Failed to write wrapper script")?; // Make executable Command::new("chmod") .arg("+x") .arg(&temp_script) .output() .context("Failed to make script executable")?; // Run script in a temporary Alpine container let output = Command::new("podman") .arg("run") .arg("--rm") .arg("--volume") .arg(format!("{}:/script.sh:ro", temp_script)) .arg("alpine:latest") .arg("sh") .arg("/script.sh") .output() .context("Failed to execute Parmanode script in container")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Parmanode script failed: {}", stderr)); } info!("Parmanode script completed successfully"); Ok(()) } /// Install a Parmanode module (runs script and sets up container) pub async fn install_module(&self, module_path: &PathBuf) -> Result { // Find the main installation script let install_script = module_path.join("install.sh"); if !install_script.exists() { return Err(anyhow::anyhow!("No install.sh found in Parmanode module")); } // Run the installation script self.run_script(&install_script).await?; // Try to convert to app manifest for future management let converter = crate::converter::ParmanodeConverter::new(); match converter.convert_to_manifest(module_path).await { Ok(manifest) => { info!("Converted Parmanode module to app manifest"); // TODO: Save manifest for future use Ok(manifest.app.id) } Err(e) => { warn!("Failed to convert Parmanode module: {}", e); // Return a generic ID Ok(format!("parmanode-{}", module_path.file_name() .and_then(|n| n.to_str()) .unwrap_or("module"))) } } } }