pupoxide 0.2.2

A high-performance, memory-safe, declarative configuration management tool inspired by Puppet.
Documentation
use crate::application::{
    EnvironmentLoader, ProviderRegistry, PupoxideEngine, execute_transaction,
};
use crate::interface::cli::MasterAction;
use crate::interface::utils::resolve_module_path;
use anyhow::{Result, anyhow};
use std::path::PathBuf;
use std::sync::Arc;
use tracing::{error, info, warn};

pub async fn handle_run(
    file: PathBuf,
    module_path: Option<PathBuf>,
    dry_run: bool,
    show_unchanged: bool,
) -> Result<()> {
    let state_dir = PathBuf::from("/tmp/pupoxide");
    let state_store = crate::infrastructure::StateStore::new(state_dir.join("state"));

    let mut provider_registry = ProviderRegistry::new();
    provider_registry.register(Arc::new(crate::infrastructure::FsAdapter));
    provider_registry.register(Arc::new(crate::infrastructure::ExecAdapter));
    provider_registry.register(Arc::new(crate::infrastructure::PackageAdapter::default()));
    let provider = Arc::new(provider_registry);

    let engine = PupoxideEngine::new(None);

    if let Some(mp) = resolve_module_path(&file, module_path) {
        engine.set_module_path(mp);
    }

    let facts = crate::infrastructure::Facter::collect();
    let catalog = engine.run_manifest(file, "localhost".to_string(), "local".to_string(), facts)?;

    crate::interface::formatter::PrettyFormatter::print_header();
    let _reports = execute_transaction(catalog, &state_store, provider, dry_run, |report| {
        if show_unchanged || report.status != crate::domain::report::ResourceStatus::Unchanged {
            println!(
                "{}",
                crate::interface::formatter::PrettyFormatter::format_line(report)
            );
        }
    })
    .await?;

    Ok(())
}

pub async fn handle_apply(
    environment: String,
    dry_run: bool,
    show_unchanged: bool,
    config: PathBuf,
) -> Result<()> {
    let state_dir = PathBuf::from("/tmp/pupoxide");
    let state_store = crate::infrastructure::StateStore::new(state_dir.join("state"));

    let mut provider_registry = ProviderRegistry::new();
    provider_registry.register(Arc::new(crate::infrastructure::FsAdapter));
    provider_registry.register(Arc::new(crate::infrastructure::ExecAdapter));
    provider_registry.register(Arc::new(crate::infrastructure::PackageAdapter::default()));
    let provider = Arc::new(provider_registry);

    let loader = EnvironmentLoader::new(config);
    let manifest_path = loader.get_site_manifest(&environment)?;
    let modules_path = loader.get_modules_path(&environment);

    let mut stash = None;
    let env_path = modules_path
        .parent()
        .ok_or_else(|| anyhow!("Environment path must have a parent"))?
        .to_path_buf();

    match crate::infrastructure::Stash::new(env_path) {
        Ok(Some(s)) => stash = Some(Arc::new(s) as Arc<dyn crate::application::StashProvider>),
        Ok(None) => {}
        Err(e) => warn!("Failed to load Stash: {}", e),
    }

    let engine = PupoxideEngine::new(stash);
    let facts = crate::infrastructure::Facter::collect();
    let catalog = engine.run_manifest_with_modules(
        manifest_path,
        modules_path,
        "localhost".to_string(),
        environment,
        facts,
    )?;

    crate::interface::formatter::PrettyFormatter::print_header();
    let _reports = execute_transaction(catalog, &state_store, provider, dry_run, |report| {
        if show_unchanged || report.status != crate::domain::report::ResourceStatus::Unchanged {
            println!(
                "{}",
                crate::interface::formatter::PrettyFormatter::format_line(report)
            );
        }
    })
    .await?;

    Ok(())
}

pub async fn handle_master(
    action: MasterAction,
    config: Option<PathBuf>,
    default_config: PathBuf,
) -> Result<()> {
    let config_dir = config.unwrap_or(default_config);

    tokio::fs::create_dir_all(&config_dir).await?;

    let loader = EnvironmentLoader::new(config_dir.clone());
    let engine = PupoxideEngine::new(None);

    let certs_dir = config_dir.join("certs");
    tokio::fs::create_dir_all(&certs_dir).await?;

    let ca_cert_path = certs_dir.join("ca.pem");
    let ca_key_path = certs_dir.join("ca.key");
    let ca = crate::infrastructure::CertificateAuthority::new_or_load(&ca_cert_path, &ca_key_path)?;

    ca.save(&ca_cert_path, &ca_key_path)?;

    let bootstrap_requests_dir = certs_dir.join("bootstrap_requests");
    let agents_dir = certs_dir.join("agents");

    tokio::fs::create_dir_all(&bootstrap_requests_dir).await?;
    tokio::fs::create_dir_all(&agents_dir).await?;

    let bootstrap_manager =
        crate::infrastructure::BootstrapRequestManager::new(bootstrap_requests_dir);
    let agent_registry = crate::infrastructure::AgentRegistryFs::new(agents_dir);

    let state = crate::interface::server::MasterState {
        engine,
        loader,
        ca,
        bootstrap_manager,
        agent_registry,
    };

    match action {
        MasterAction::Start { port } => {
            crate::interface::server::start_master(state, port).await?;
        }
        MasterAction::Sign { node } => {
            let request = state.bootstrap_manager.get_request(&node).await?;

            if !request.is_pending() {
                error!(
                    "Request for node {} is not pending (status: {})",
                    node, request.status
                );
                anyhow::bail!(
                    "Request for node {} cannot be signed (status: {})",
                    node,
                    request.status
                );
            }

            let cert_pem = state.ca.sign_csr(&node, 365)?;
            state.bootstrap_manager.approve_request(&node).await?;
            state
                .agent_registry
                .register(&node, &node, cert_pem)
                .await?;

            info!("Successfully signed and registered node: {}", node);
            println!("✓ Node '{}' has been approved and registered", node);
        }
        MasterAction::Reject { node } => {
            let request = state.bootstrap_manager.get_request(&node).await?;

            if !request.is_pending() {
                error!(
                    "Request for node {} is not pending (status: {})",
                    node, request.status
                );
                anyhow::bail!(
                    "Request for node {} cannot be rejected (status: {})",
                    node,
                    request.status
                );
            }

            state.bootstrap_manager.reject_request(&node).await?;

            info!("Rejected bootstrap request for node: {}", node);
            println!("✓ Request for node '{}' has been rejected", node);
        }
        MasterAction::List => {
            let requests = state.bootstrap_manager.list_pending_requests().await?;

            if requests.is_empty() {
                println!("No pending bootstrap requests");
            } else {
                println!("\nPending Bootstrap Requests:");
                println!("{:-<60}", "");
                println!("{:<20} {:<20} {:<10}", "Node ID", "Requested At", "Status");
                println!("{:-<60}", "");

                for req in requests {
                    let dt = chrono::DateTime::<chrono::Utc>::from_timestamp(req.requested_at, 0)
                        .unwrap_or_default();
                    println!(
                        "{:<20} {:<20} {:<10}",
                        req.node_id,
                        dt.format("%Y-%m-%d %H:%M:%S"),
                        req.status
                    );
                }
            }
        }
    }
    Ok(())
}

pub struct AgentOptions {
    pub server: String,
    pub node: String,
    pub environment: String,
    pub bootstrap: bool,
    pub check: bool,
    pub check_timeout: u64,
    pub dry_run: bool,
    pub show_unchanged: bool,
    pub cert_dir: Option<PathBuf>,
}

pub async fn handle_agent(opts: AgentOptions) -> Result<()> {
    let agent = crate::interface::agent::PupoxideAgent::new(
        opts.server,
        opts.node,
        opts.environment,
        opts.cert_dir,
    );

    if opts.check {
        agent.check_bootstrap_status(opts.check_timeout).await?;
    } else if opts.bootstrap {
        agent.bootstrap().await?;
    } else {
        agent.run(opts.dry_run, opts.show_unchanged).await?;
    }
    Ok(())
}

pub async fn handle_graph(
    file: PathBuf,
    module_path: Option<PathBuf>,
    filter: Option<Vec<String>>,
    max_depth: usize,
    style: String,
) -> Result<()> {
    let engine = PupoxideEngine::new(None);

    if let Some(mp) = resolve_module_path(&file, module_path) {
        engine.set_module_path(mp);
    }

    let facts = crate::infrastructure::Facter::collect();
    let catalog = engine.run_manifest(file, "localhost".to_string(), "local".to_string(), facts)?;

    let style = match style.as_str() {
        "mermaid" => crate::interface::graph::GraphStyle::Mermaid,
        _ => crate::interface::graph::GraphStyle::Ascii,
    };

    crate::interface::graph::display_graph(&catalog, filter.as_deref(), max_depth, style)?;
    Ok(())
}