mid coding commit
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
use crate::api::rpc::RpcHandler;
|
||||
use crate::config::Config;
|
||||
use anyhow::Result;
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use hyper::body::Bytes;
|
||||
use hyper::{Method, Request, Response, StatusCode};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use std::sync::Arc;
|
||||
use tracing::debug;
|
||||
|
||||
pub struct ApiHandler {
|
||||
config: Config,
|
||||
rpc_handler: Arc<RpcHandler>,
|
||||
// Add other handlers here (websocket, static files, etc.)
|
||||
}
|
||||
|
||||
impl ApiHandler {
|
||||
pub async fn new(config: Config) -> Result<Self> {
|
||||
let rpc_handler = Arc::new(RpcHandler::new(config.clone()).await?);
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
rpc_handler,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn handle_request(
|
||||
&self,
|
||||
req: Request<http_body_util::Body<Bytes>>,
|
||||
) -> Result<Response<Full<Bytes>>> {
|
||||
let path = req.uri().path();
|
||||
let method = req.method();
|
||||
|
||||
// Convert Incoming body to bytes
|
||||
let (parts, body) = req.into_parts();
|
||||
let collected = body.collect().await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to read body: {}", e))?;
|
||||
let body_bytes = collected.to_bytes();
|
||||
|
||||
// Reconstruct request with Full<Bytes> body for RPC handler
|
||||
let req_with_bytes = Request::from_parts(parts, Full::new(body_bytes));
|
||||
|
||||
debug!("{} {}", method, path);
|
||||
|
||||
// Route requests
|
||||
match (method, path) {
|
||||
(&Method::POST, "/rpc/v1") => {
|
||||
self.rpc_handler.handle(req_with_bytes).await
|
||||
}
|
||||
(&Method::GET, "/health") => {
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body(Full::new(Bytes::from("OK")))
|
||||
.unwrap())
|
||||
}
|
||||
_ => {
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Full::new(Bytes::from("Not Found")))
|
||||
.unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mod handler;
|
||||
mod rpc;
|
||||
|
||||
pub use handler::ApiHandler;
|
||||
pub use rpc::RpcHandler;
|
||||
@@ -0,0 +1,336 @@
|
||||
use crate::config::Config;
|
||||
use crate::container::DevContainerOrchestrator;
|
||||
use anyhow::{Context, Result};
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use hyper::body::Bytes;
|
||||
use hyper::{Request, Response, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RpcRequest {
|
||||
method: String,
|
||||
params: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct RpcResponse {
|
||||
result: Option<serde_json::Value>,
|
||||
error: Option<RpcError>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct RpcError {
|
||||
code: i32,
|
||||
message: String,
|
||||
data: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
pub struct RpcHandler {
|
||||
config: Config,
|
||||
orchestrator: Option<Arc<DevContainerOrchestrator>>,
|
||||
}
|
||||
|
||||
impl RpcHandler {
|
||||
pub async fn new(config: Config) -> Result<Self> {
|
||||
let orchestrator = if config.dev_mode {
|
||||
Some(Arc::new(
|
||||
DevContainerOrchestrator::new(config.clone()).await?,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
orchestrator,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn handle(
|
||||
&self,
|
||||
req: Request<Full<Bytes>>,
|
||||
) -> Result<Response<Full<Bytes>>> {
|
||||
// Read request body - Full<Bytes> is already collected
|
||||
let (_, body) = req.into_parts();
|
||||
// Full<Bytes> implements Body, collect it to get the bytes
|
||||
use http_body_util::BodyExt;
|
||||
let collected = body.collect().await
|
||||
.context("Failed to collect body")?;
|
||||
let body_bytes = collected.to_bytes();
|
||||
|
||||
let rpc_req: RpcRequest = serde_json::from_slice(&body_bytes)
|
||||
.context("Invalid RPC request")?;
|
||||
|
||||
debug!("RPC method: {}", rpc_req.method);
|
||||
|
||||
// Route to handler
|
||||
let result = match rpc_req.method.as_str() {
|
||||
"echo" => self.handle_echo(rpc_req.params).await,
|
||||
"server.echo" => self.handle_echo(rpc_req.params).await,
|
||||
"container-install" => self.handle_container_install(rpc_req.params).await,
|
||||
"container-start" => self.handle_container_start(rpc_req.params).await,
|
||||
"container-stop" => self.handle_container_stop(rpc_req.params).await,
|
||||
"container-remove" => self.handle_container_remove(rpc_req.params).await,
|
||||
"container-list" => self.handle_container_list().await,
|
||||
"container-status" => self.handle_container_status(rpc_req.params).await,
|
||||
"container-logs" => self.handle_container_logs(rpc_req.params).await,
|
||||
"container-health" => self.handle_container_health(rpc_req.params).await,
|
||||
_ => {
|
||||
Err(anyhow::anyhow!("Unknown method: {}", rpc_req.method))
|
||||
}
|
||||
};
|
||||
|
||||
// Build response
|
||||
let rpc_resp = match result {
|
||||
Ok(data) => RpcResponse {
|
||||
result: Some(data),
|
||||
error: None,
|
||||
},
|
||||
Err(e) => {
|
||||
error!("RPC error: {}", e);
|
||||
RpcResponse {
|
||||
result: None,
|
||||
error: Some(RpcError {
|
||||
code: -1,
|
||||
message: e.to_string(),
|
||||
data: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let body = serde_json::to_vec(&rpc_resp)
|
||||
.context("Failed to serialize response")?;
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Full::new(Bytes::from(body)))
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
async fn handle_echo(&self, params: Option<serde_json::Value>) -> Result<serde_json::Value> {
|
||||
if let Some(p) = params {
|
||||
if let Some(msg) = p.get("message").and_then(|v| v.as_str()) {
|
||||
return Ok(serde_json::json!({ "message": msg }));
|
||||
}
|
||||
}
|
||||
Ok(serde_json::json!({ "message": "Hello from Archipelago!" }))
|
||||
}
|
||||
|
||||
async fn handle_container_install(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?;
|
||||
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let manifest_path = params
|
||||
.get("manifest_path")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing manifest_path"))?;
|
||||
|
||||
// Load manifest
|
||||
let manifest_content = tokio::fs::read_to_string(manifest_path)
|
||||
.await
|
||||
.context("Failed to read manifest file")?;
|
||||
let manifest: archipelago_container::AppManifest = serde_yaml::from_str(&manifest_content)
|
||||
.context("Failed to parse manifest")?;
|
||||
|
||||
let container_name = orchestrator
|
||||
.install_container(&manifest, manifest_path)
|
||||
.await
|
||||
.context("Failed to install container")?;
|
||||
|
||||
Ok(serde_json::json!(container_name))
|
||||
}
|
||||
|
||||
async fn handle_container_start(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?;
|
||||
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let app_id = params
|
||||
.get("app_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing app_id"))?;
|
||||
|
||||
orchestrator
|
||||
.start_container(app_id)
|
||||
.await
|
||||
.context("Failed to start container")?;
|
||||
|
||||
Ok(serde_json::json!({ "status": "started" }))
|
||||
}
|
||||
|
||||
async fn handle_container_stop(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?;
|
||||
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let app_id = params
|
||||
.get("app_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing app_id"))?;
|
||||
|
||||
orchestrator
|
||||
.stop_container(app_id)
|
||||
.await
|
||||
.context("Failed to stop container")?;
|
||||
|
||||
Ok(serde_json::json!({ "status": "stopped" }))
|
||||
}
|
||||
|
||||
async fn handle_container_remove(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?;
|
||||
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let app_id = params
|
||||
.get("app_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing app_id"))?;
|
||||
let preserve_data = params
|
||||
.get("preserve_data")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
orchestrator
|
||||
.remove_container(app_id, preserve_data)
|
||||
.await
|
||||
.context("Failed to remove container")?;
|
||||
|
||||
Ok(serde_json::json!({ "status": "removed" }))
|
||||
}
|
||||
|
||||
async fn handle_container_list(&self) -> Result<serde_json::Value> {
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?;
|
||||
|
||||
let containers = orchestrator
|
||||
.list_containers()
|
||||
.await
|
||||
.context("Failed to list containers")?;
|
||||
|
||||
Ok(serde_json::to_value(containers)?)
|
||||
}
|
||||
|
||||
async fn handle_container_status(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?;
|
||||
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let app_id = params
|
||||
.get("app_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing app_id"))?;
|
||||
|
||||
let status = orchestrator
|
||||
.get_container_status(app_id)
|
||||
.await
|
||||
.context("Failed to get container status")?;
|
||||
|
||||
Ok(serde_json::to_value(status)?)
|
||||
}
|
||||
|
||||
async fn handle_container_logs(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?;
|
||||
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let app_id = params
|
||||
.get("app_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing app_id"))?;
|
||||
let lines = params
|
||||
.get("lines")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(100) as u32;
|
||||
|
||||
let logs = orchestrator
|
||||
.get_container_logs(app_id, lines)
|
||||
.await
|
||||
.context("Failed to get container logs")?;
|
||||
|
||||
Ok(serde_json::to_value(logs)?)
|
||||
}
|
||||
|
||||
async fn handle_container_health(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?;
|
||||
|
||||
// If app_id is provided, get health for that app
|
||||
if let Some(params) = params {
|
||||
if let Some(app_id) = params.get("app_id").and_then(|v| v.as_str()) {
|
||||
let health = orchestrator
|
||||
.get_health_status(app_id)
|
||||
.await
|
||||
.context("Failed to get container health")?;
|
||||
return Ok(serde_json::json!({ app_id: health }));
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, get health for all containers
|
||||
let containers = orchestrator
|
||||
.list_containers()
|
||||
.await
|
||||
.context("Failed to list containers")?;
|
||||
|
||||
let mut health_map = serde_json::Map::new();
|
||||
for container in containers {
|
||||
// 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") {
|
||||
match orchestrator.get_health_status(app_id).await {
|
||||
Ok(health) => {
|
||||
health_map.insert(app_id.to_string(), serde_json::Value::String(health));
|
||||
}
|
||||
Err(_) => {
|
||||
health_map.insert(app_id.to_string(), serde_json::Value::String("unknown".to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(serde_json::Value::Object(health_map))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Authentication module for Archipelago
|
||||
// Handles user setup, onboarding, and login
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub password_hash: String,
|
||||
pub setup_complete: bool,
|
||||
pub onboarding_complete: bool,
|
||||
}
|
||||
|
||||
pub struct AuthManager {
|
||||
data_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl AuthManager {
|
||||
pub fn new(data_dir: PathBuf) -> Self {
|
||||
Self { data_dir }
|
||||
}
|
||||
|
||||
pub async fn is_setup(&self) -> Result<bool> {
|
||||
let user_file = self.data_dir.join("user.json");
|
||||
Ok(user_file.exists())
|
||||
}
|
||||
|
||||
pub async fn get_user(&self) -> Result<Option<User>> {
|
||||
let user_file = self.data_dir.join("user.json");
|
||||
if !user_file.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&user_file).await?;
|
||||
let user: User = serde_json::from_str(&content)?;
|
||||
Ok(Some(user))
|
||||
}
|
||||
|
||||
pub async fn setup_user(&self, password: &str) -> Result<()> {
|
||||
use bcrypt::{hash, DEFAULT_COST};
|
||||
|
||||
let password_hash = hash(password, DEFAULT_COST)?;
|
||||
|
||||
let user = User {
|
||||
password_hash,
|
||||
setup_complete: true,
|
||||
onboarding_complete: false,
|
||||
};
|
||||
|
||||
let user_file = self.data_dir.join("user.json");
|
||||
let content = serde_json::to_string_pretty(&user)?;
|
||||
fs::write(&user_file, content).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn complete_onboarding(&self) -> Result<()> {
|
||||
if let Some(mut user) = self.get_user().await? {
|
||||
user.onboarding_complete = true;
|
||||
let user_file = self.data_dir.join("user.json");
|
||||
let content = serde_json::to_string_pretty(&user)?;
|
||||
fs::write(&user_file, content).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn verify_password(&self, password: &str) -> Result<bool> {
|
||||
use bcrypt::verify;
|
||||
|
||||
if let Some(user) = self.get_user().await? {
|
||||
Ok(verify(password, &user.password_hash)?)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::fs;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ContainerRuntime {
|
||||
Podman,
|
||||
Docker,
|
||||
Auto,
|
||||
}
|
||||
|
||||
impl ContainerRuntime {
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"podman" => ContainerRuntime::Podman,
|
||||
"docker" => ContainerRuntime::Docker,
|
||||
"auto" | _ => ContainerRuntime::Auto,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum BitcoinSimulation {
|
||||
Mock,
|
||||
Testnet,
|
||||
Mainnet,
|
||||
None,
|
||||
}
|
||||
|
||||
impl BitcoinSimulation {
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"mock" => BitcoinSimulation::Mock,
|
||||
"testnet" => BitcoinSimulation::Testnet,
|
||||
"mainnet" => BitcoinSimulation::Mainnet,
|
||||
"none" | _ => BitcoinSimulation::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub data_dir: PathBuf,
|
||||
pub bind_host: String,
|
||||
pub bind_port: u16,
|
||||
pub log_level: String,
|
||||
// Dev mode configuration
|
||||
pub dev_mode: bool,
|
||||
pub container_runtime: ContainerRuntime,
|
||||
pub port_offset: u16,
|
||||
pub bitcoin_simulation: BitcoinSimulation,
|
||||
pub dev_data_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub async fn load() -> Result<Self> {
|
||||
// Default configuration
|
||||
let mut config = Self::default();
|
||||
|
||||
// Try to load from config file
|
||||
let config_path = Path::new("/etc/archipelago/config.toml");
|
||||
if config_path.exists() {
|
||||
let content = fs::read_to_string(config_path).await
|
||||
.context("Failed to read config file")?;
|
||||
let file_config: Config = toml::de::from_str(&content)
|
||||
.context("Failed to parse config file")?;
|
||||
config = file_config;
|
||||
}
|
||||
|
||||
// Override with environment variables
|
||||
if let Ok(data_dir) = std::env::var("ARCHIPELAGO_DATA_DIR") {
|
||||
config.data_dir = PathBuf::from(data_dir);
|
||||
}
|
||||
|
||||
if let Ok(bind) = std::env::var("ARCHIPELAGO_BIND") {
|
||||
let parts: Vec<&str> = bind.split(':').collect();
|
||||
if parts.len() == 2 {
|
||||
config.bind_host = parts[0].to_string();
|
||||
config.bind_port = parts[1].parse()
|
||||
.context("Invalid port in ARCHIPELAGO_BIND")?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(level) = std::env::var("ARCHIPELAGO_LOG_LEVEL") {
|
||||
config.log_level = level;
|
||||
}
|
||||
|
||||
// Dev mode configuration
|
||||
if let Ok(dev_mode) = std::env::var("ARCHIPELAGO_DEV_MODE") {
|
||||
config.dev_mode = dev_mode.parse().unwrap_or(false);
|
||||
}
|
||||
|
||||
if let Ok(runtime) = std::env::var("ARCHIPELAGO_CONTAINER_RUNTIME") {
|
||||
config.container_runtime = ContainerRuntime::from_str(&runtime);
|
||||
}
|
||||
|
||||
if let Ok(offset) = std::env::var("ARCHIPELAGO_PORT_OFFSET") {
|
||||
config.port_offset = offset.parse()
|
||||
.context("Invalid port offset in ARCHIPELAGO_PORT_OFFSET")?;
|
||||
}
|
||||
|
||||
if let Ok(sim) = std::env::var("ARCHIPELAGO_BITCOIN_SIMULATION") {
|
||||
config.bitcoin_simulation = BitcoinSimulation::from_str(&sim);
|
||||
}
|
||||
|
||||
if let Ok(dev_data_dir) = std::env::var("ARCHIPELAGO_DEV_DATA_DIR") {
|
||||
config.dev_data_dir = PathBuf::from(dev_data_dir);
|
||||
}
|
||||
|
||||
// Ensure data directory exists
|
||||
fs::create_dir_all(&config.data_dir).await
|
||||
.context("Failed to create data directory")?;
|
||||
|
||||
// Ensure dev data directory exists if in dev mode
|
||||
if config.dev_mode {
|
||||
fs::create_dir_all(&config.dev_data_dir).await
|
||||
.context("Failed to create dev data directory")?;
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
data_dir: PathBuf::from("/var/lib/archipelago"),
|
||||
bind_host: "127.0.0.1".to_string(),
|
||||
bind_port: 5959,
|
||||
log_level: "info".to_string(),
|
||||
dev_mode: false,
|
||||
container_runtime: ContainerRuntime::Auto,
|
||||
port_offset: 10000,
|
||||
bitcoin_simulation: BitcoinSimulation::Mock,
|
||||
dev_data_dir: PathBuf::from("/tmp/archipelago-dev"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,48 @@
|
||||
// Archipelago Bitcoin Node OS - Native Backend
|
||||
// Pure Archipelago implementation, no StartOS dependencies
|
||||
|
||||
use anyhow::Result;
|
||||
use std::net::SocketAddr;
|
||||
use tracing::{info, error};
|
||||
|
||||
mod api;
|
||||
mod auth;
|
||||
mod config;
|
||||
mod container;
|
||||
mod server;
|
||||
|
||||
use config::Config;
|
||||
use server::Server;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Initialize tracing
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| "archipelago=debug,info".into()),
|
||||
)
|
||||
.init();
|
||||
|
||||
info!("🚀 Starting Archipelago Bitcoin Node OS");
|
||||
|
||||
// Load configuration
|
||||
let config = Config::load().await?;
|
||||
info!("📁 Data directory: {}", config.data_dir.display());
|
||||
|
||||
// Create server
|
||||
let server = Server::new(config.clone()).await?;
|
||||
|
||||
// Start server
|
||||
let addr: SocketAddr = format!("{}:{}", config.bind_host, config.bind_port)
|
||||
.parse()
|
||||
.expect("Invalid bind address");
|
||||
|
||||
info!("🌐 Server listening on http://{}", addr);
|
||||
info!("📡 RPC API: http://{}/rpc/v1", addr);
|
||||
info!("🔌 WebSocket: ws://{}/ws", addr);
|
||||
|
||||
server.serve(addr).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
use crate::api::ApiHandler;
|
||||
use crate::config::Config;
|
||||
use anyhow::Result;
|
||||
use hyper_util::rt::TokioIo;
|
||||
use hyper_util::server::conn::auto::Builder as AutoBuilder;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpListener;
|
||||
use hyper::service::service_fn;
|
||||
use tracing::{error, info};
|
||||
|
||||
pub struct Server {
|
||||
config: Config,
|
||||
api_handler: Arc<ApiHandler>,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub async fn new(config: Config) -> Result<Self> {
|
||||
let api_handler = Arc::new(ApiHandler::new(config.clone()).await?);
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
api_handler,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn serve(&self, addr: SocketAddr) -> Result<()> {
|
||||
let listener = TcpListener::bind(addr).await?;
|
||||
|
||||
loop {
|
||||
let (stream, peer_addr) = match listener.accept().await {
|
||||
Ok(conn) => conn,
|
||||
Err(e) => {
|
||||
error!("Failed to accept connection: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let io = TokioIo::new(stream);
|
||||
let handler = self.api_handler.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let service = service_fn(move |req| {
|
||||
let handler = handler.clone();
|
||||
async move {
|
||||
handler.handle_request(req).await
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
|
||||
}
|
||||
});
|
||||
|
||||
let builder = AutoBuilder::new(
|
||||
hyper_util::rt::TokioExecutor::new()
|
||||
);
|
||||
if let Err(e) = builder
|
||||
.serve_connection(io, service)
|
||||
.await
|
||||
{
|
||||
error!("Error serving connection from {}: {}", peer_addr, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user