mid coding commit
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::fs;
|
||||
|
||||
pub struct DevDataManager {
|
||||
dev_data_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl DevDataManager {
|
||||
pub fn new(dev_data_dir: PathBuf) -> Self {
|
||||
Self { dev_data_dir }
|
||||
}
|
||||
|
||||
/// Get the dev data directory for an app
|
||||
pub fn get_app_data_dir(&self, app_id: &str) -> PathBuf {
|
||||
self.dev_data_dir.join(app_id)
|
||||
}
|
||||
|
||||
/// Create data directory for an app
|
||||
pub async fn create_app_data_dir(&self, app_id: &str) -> Result<PathBuf> {
|
||||
let app_dir = self.get_app_data_dir(app_id);
|
||||
fs::create_dir_all(&app_dir)
|
||||
.await
|
||||
.with_context(|| format!("Failed to create app data directory: {:?}", app_dir))?;
|
||||
Ok(app_dir)
|
||||
}
|
||||
|
||||
/// Map a volume source path to dev path
|
||||
pub fn map_volume_path(&self, app_id: &str, volume_source: &str) -> PathBuf {
|
||||
// If volume source is already in dev_data_dir, use it as-is
|
||||
if volume_source.starts_with(self.dev_data_dir.to_str().unwrap_or("")) {
|
||||
PathBuf::from(volume_source)
|
||||
} else {
|
||||
// Map production path to dev path
|
||||
// e.g., /var/lib/archipelago/bitcoin -> /tmp/archipelago-dev/bitcoin
|
||||
let app_dir = self.get_app_data_dir(app_id);
|
||||
|
||||
// Extract the relative path from the production path
|
||||
if let Some(relative) = volume_source.strip_prefix("/var/lib/archipelago/") {
|
||||
app_dir.join(relative)
|
||||
} else if let Some(relative) = volume_source.strip_prefix("/var/lib/archipelago") {
|
||||
app_dir.join(relative)
|
||||
} else {
|
||||
// If it doesn't match expected pattern, use app_id as base
|
||||
app_dir.join(volume_source.trim_start_matches('/'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clean up app data directory
|
||||
pub async fn cleanup_app_data(&self, app_id: &str) -> Result<()> {
|
||||
let app_dir = self.get_app_data_dir(app_id);
|
||||
if app_dir.exists() {
|
||||
fs::remove_dir_all(&app_dir)
|
||||
.await
|
||||
.with_context(|| format!("Failed to remove app data directory: {:?}", app_dir))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Preserve app data (no-op for cleanup, used when removing container)
|
||||
pub async fn preserve_app_data(&self, _app_id: &str) -> Result<()> {
|
||||
// In dev mode, we might want to preserve data between container removals
|
||||
// This is a no-op by default, but can be extended
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all app data directories
|
||||
pub async fn list_app_data_dirs(&self) -> Result<Vec<String>> {
|
||||
if !self.dev_data_dir.exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let mut entries = fs::read_dir(&self.dev_data_dir)
|
||||
.await
|
||||
.with_context(|| format!("Failed to read dev data directory: {:?}", self.dev_data_dir))?;
|
||||
|
||||
let mut app_ids = Vec::new();
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
if entry.file_type().await?.is_dir() {
|
||||
if let Some(name) = entry.file_name().to_str() {
|
||||
app_ids.push(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(app_ids)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_map_volume_path() {
|
||||
let temp_dir = std::env::temp_dir().join("test-archipelago");
|
||||
let manager = DevDataManager::new(temp_dir.clone());
|
||||
|
||||
let dev_path = manager.map_volume_path("bitcoin-core", "/var/lib/archipelago/bitcoin");
|
||||
assert!(dev_path.to_string_lossy().contains("bitcoin-core"));
|
||||
|
||||
// Cleanup
|
||||
let _ = tokio::fs::remove_dir_all(&temp_dir).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_app_data_dir() {
|
||||
let temp_dir = std::env::temp_dir().join("test-archipelago-2");
|
||||
let manager = DevDataManager::new(temp_dir.clone());
|
||||
|
||||
let app_dir = manager.create_app_data_dir("test-app").await.unwrap();
|
||||
assert!(app_dir.exists());
|
||||
|
||||
// Cleanup
|
||||
let _ = tokio::fs::remove_dir_all(&temp_dir).await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
use archipelago_container::{
|
||||
AppManifest, BitcoinSimulator, BitcoinSimulationMode, ContainerRuntime as ContainerRuntimeTrait,
|
||||
ContainerStatus, PortManager,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::config::{Config, ContainerRuntime, BitcoinSimulation};
|
||||
use crate::container::data_manager::DevDataManager;
|
||||
|
||||
pub struct DevContainerOrchestrator {
|
||||
runtime: Arc<dyn ContainerRuntimeTrait>,
|
||||
port_manager: Arc<PortManager>,
|
||||
bitcoin_simulator: Arc<BitcoinSimulator>,
|
||||
data_manager: Arc<DevDataManager>,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl DevContainerOrchestrator {
|
||||
pub async fn new(config: Config) -> Result<Self> {
|
||||
let user = std::env::var("USER").unwrap_or_else(|_| "archipelago".to_string());
|
||||
|
||||
// Create runtime based on config
|
||||
let runtime: Arc<dyn ContainerRuntimeTrait> = match &config.container_runtime {
|
||||
ContainerRuntime::Podman => {
|
||||
Arc::new(archipelago_container::PodmanRuntime::new(user.clone()))
|
||||
}
|
||||
ContainerRuntime::Docker => {
|
||||
Arc::new(archipelago_container::DockerRuntime::new(user.clone()))
|
||||
}
|
||||
ContainerRuntime::Auto => {
|
||||
Arc::new(
|
||||
archipelago_container::AutoRuntime::new(user.clone())
|
||||
.await
|
||||
.context("Failed to create auto runtime")?,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let port_manager = Arc::new(PortManager::new(config.port_offset));
|
||||
let bitcoin_simulator = Arc::new(BitcoinSimulator::new(
|
||||
BitcoinSimulationMode::from(
|
||||
match &config.bitcoin_simulation {
|
||||
BitcoinSimulation::Mock => "mock",
|
||||
BitcoinSimulation::Testnet => "testnet",
|
||||
BitcoinSimulation::Mainnet => "mainnet",
|
||||
BitcoinSimulation::None => "none",
|
||||
}
|
||||
),
|
||||
));
|
||||
let data_manager = Arc::new(DevDataManager::new(config.dev_data_dir.clone()));
|
||||
|
||||
Ok(Self {
|
||||
runtime,
|
||||
port_manager,
|
||||
bitcoin_simulator,
|
||||
data_manager,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
/// Install a container from a manifest
|
||||
pub async fn install_container(
|
||||
&self,
|
||||
manifest: &AppManifest,
|
||||
manifest_path: &str,
|
||||
) -> Result<String> {
|
||||
let app_id = &manifest.app.id;
|
||||
let container_name = format!("archipelago-{}-dev", app_id);
|
||||
|
||||
// Check dependencies
|
||||
if self.config.dev_mode {
|
||||
// In dev mode, check if Bitcoin dependency can be satisfied
|
||||
for dep in &manifest.app.dependencies {
|
||||
if let archipelago_container::Dependency::App {
|
||||
app_id: dep_id,
|
||||
version: _,
|
||||
} = dep
|
||||
{
|
||||
if dep_id == "bitcoin-core" {
|
||||
if !self.bitcoin_simulator.is_bitcoin_available() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Bitcoin Core dependency not satisfied (simulation: {:?})",
|
||||
self.bitcoin_simulator.mode()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate ports
|
||||
let base_ports: Vec<u16> = manifest.app.ports.iter().map(|p| p.host).collect();
|
||||
let _dev_ports = self
|
||||
.port_manager
|
||||
.allocate_ports(app_id, &base_ports)
|
||||
.context("Failed to allocate ports")?;
|
||||
|
||||
// Create app data directory
|
||||
self.data_manager
|
||||
.create_app_data_dir(app_id)
|
||||
.await
|
||||
.context("Failed to create app data directory")?;
|
||||
|
||||
// Map volumes to dev paths
|
||||
let mut dev_manifest = manifest.clone();
|
||||
for volume in &mut dev_manifest.app.volumes {
|
||||
let dev_path = self.data_manager.map_volume_path(app_id, &volume.source);
|
||||
volume.source = dev_path.to_string_lossy().to_string();
|
||||
}
|
||||
|
||||
// Pull image
|
||||
self.runtime
|
||||
.pull_image(
|
||||
&manifest.app.container.image,
|
||||
manifest.app.container.image_signature.as_deref(),
|
||||
)
|
||||
.await
|
||||
.context("Failed to pull image")?;
|
||||
|
||||
// Create container with port offset
|
||||
let port_offset = if self.config.dev_mode {
|
||||
self.config.port_offset
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
self.runtime
|
||||
.create_container(&dev_manifest, &container_name, port_offset)
|
||||
.await
|
||||
.context("Failed to create container")?;
|
||||
|
||||
Ok(container_name)
|
||||
}
|
||||
|
||||
/// Start a container
|
||||
pub async fn start_container(&self, app_id: &str) -> Result<()> {
|
||||
let container_name = format!("archipelago-{}-dev", app_id);
|
||||
self.runtime
|
||||
.start_container(&container_name)
|
||||
.await
|
||||
.context("Failed to start container")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop a container
|
||||
pub async fn stop_container(&self, app_id: &str) -> Result<()> {
|
||||
let container_name = format!("archipelago-{}-dev", app_id);
|
||||
self.runtime
|
||||
.stop_container(&container_name)
|
||||
.await
|
||||
.context("Failed to stop container")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a container
|
||||
pub async fn remove_container(&self, app_id: &str, preserve_data: bool) -> Result<()> {
|
||||
let container_name = format!("archipelago-{}-dev", app_id);
|
||||
|
||||
// Stop container first
|
||||
let _ = self.runtime.stop_container(&container_name).await;
|
||||
|
||||
// Remove container
|
||||
self.runtime
|
||||
.remove_container(&container_name)
|
||||
.await
|
||||
.context("Failed to remove container")?;
|
||||
|
||||
// Release ports
|
||||
let _ = self.port_manager.release_ports(app_id);
|
||||
|
||||
// Clean up or preserve data
|
||||
if preserve_data {
|
||||
self.data_manager.preserve_app_data(app_id).await?;
|
||||
} else {
|
||||
self.data_manager.cleanup_app_data(app_id).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get container status with dev port info
|
||||
pub async fn get_container_status(&self, app_id: &str) -> Result<ContainerStatus> {
|
||||
let container_name = format!("archipelago-{}-dev", app_id);
|
||||
let mut status = self
|
||||
.runtime
|
||||
.get_container_status(&container_name)
|
||||
.await
|
||||
.context("Failed to get container status")?;
|
||||
|
||||
// Add dev port information
|
||||
if let Some(ports) = self.port_manager.get_port_mapping(app_id) {
|
||||
status.ports = ports.iter().map(|p| p.to_string()).collect();
|
||||
}
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
/// List all containers with dev info
|
||||
pub async fn list_containers(&self) -> Result<Vec<ContainerStatus>> {
|
||||
let containers = self
|
||||
.runtime
|
||||
.list_containers()
|
||||
.await
|
||||
.context("Failed to list containers")?;
|
||||
|
||||
// Filter to only archipelago containers and add port info
|
||||
let mut result = Vec::new();
|
||||
for container in containers {
|
||||
if container.name.contains("archipelago-") {
|
||||
// Extract app_id from container name
|
||||
if let Some(app_id) = container.name.strip_prefix("archipelago-") {
|
||||
if let Some(app_id) = app_id.strip_suffix("-dev") {
|
||||
if let Some(ports) = self.port_manager.get_port_mapping(app_id) {
|
||||
let mut container_with_ports = container.clone();
|
||||
container_with_ports.ports = ports.iter().map(|p| p.to_string()).collect();
|
||||
result.push(container_with_ports);
|
||||
} else {
|
||||
result.push(container);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Get container logs
|
||||
pub async fn get_container_logs(&self, app_id: &str, lines: u32) -> Result<Vec<String>> {
|
||||
let container_name = format!("archipelago-{}-dev", app_id);
|
||||
self.runtime
|
||||
.get_container_logs(&container_name, lines)
|
||||
.await
|
||||
.context("Failed to get container logs")
|
||||
}
|
||||
|
||||
/// Get health status
|
||||
pub async fn get_health_status(&self, app_id: &str) -> Result<String> {
|
||||
let status = self.get_container_status(app_id).await?;
|
||||
match status.state {
|
||||
archipelago_container::ContainerState::Running => Ok("healthy".to_string()),
|
||||
archipelago_container::ContainerState::Stopped
|
||||
| archipelago_container::ContainerState::Exited => Ok("unhealthy".to_string()),
|
||||
archipelago_container::ContainerState::Created => Ok("starting".to_string()),
|
||||
archipelago_container::ContainerState::Paused => Ok("paused".to_string()),
|
||||
archipelago_container::ContainerState::Unknown(_) => Ok("unknown".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get port mapping for an app
|
||||
pub fn get_port_mapping(&self, app_id: &str) -> Option<Vec<u16>> {
|
||||
self.port_manager.get_port_mapping(app_id)
|
||||
}
|
||||
|
||||
/// Get Bitcoin simulator
|
||||
pub fn bitcoin_simulator(&self) -> &Arc<BitcoinSimulator> {
|
||||
&self.bitcoin_simulator
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod data_manager;
|
||||
pub mod dev_orchestrator;
|
||||
|
||||
pub use data_manager::DevDataManager;
|
||||
pub use dev_orchestrator::DevContainerOrchestrator;
|
||||
Reference in New Issue
Block a user