canic-cli 0.35.11

Operator CLI for Canic fleet backup and restore workflows
Documentation
use crate::{
    cli::defaults::local_network,
    endpoints::{
        CANDID_SERVICE_METADATA, EndpointsCommandError, EndpointsOptions,
        model::{EndpointReport, EndpointTarget},
        parse::parse_candid_service_endpoints,
    },
};
use canic_host::{
    icp::IcpCli,
    icp_config::resolve_current_canic_icp_root,
    installed_fleet::{InstalledFleetRequest, resolve_installed_fleet_from_root},
    registry::RegistryEntry,
};
use std::{
    fs,
    path::{Path, PathBuf},
};

pub(super) fn endpoint_report(
    options: &EndpointsOptions,
) -> Result<EndpointReport, EndpointsCommandError> {
    let target = resolve_endpoint_target(options);
    if let Ok(target) = &target
        && let Ok(candid) = read_live_candid(options, target)
    {
        return Ok(EndpointReport {
            source: format!("{} metadata", options.canister),
            endpoints: parse_candid_service_endpoints(&candid)?,
        });
    }

    let role = target
        .ok()
        .and_then(|target| target.role)
        .or_else(|| (!is_principal_like(&options.canister)).then(|| options.canister.clone()));
    let Some(role) = role else {
        return Err(EndpointsCommandError::NoInterfaceArtifact {
            fleet: options.fleet.clone(),
            canister: options.canister.clone(),
        });
    };
    let path = resolve_role_did(options, &role)?;
    let candid = read_did(&path)?;
    Ok(EndpointReport {
        source: path.display().to_string(),
        endpoints: parse_candid_service_endpoints(&candid)?,
    })
}

fn read_live_candid(
    options: &EndpointsOptions,
    target: &EndpointTarget,
) -> Result<String, Box<dyn std::error::Error>> {
    let root = resolve_endpoint_icp_root()?;
    Ok(IcpCli::new(&options.icp, None, options.network.clone())
        .with_cwd(root)
        .canister_metadata_output(&target.canister, CANDID_SERVICE_METADATA)?)
}

fn resolve_endpoint_target(
    options: &EndpointsOptions,
) -> Result<EndpointTarget, Box<dyn std::error::Error>> {
    if is_principal_like(&options.canister) {
        let role = load_fleet_registry(options).ok().and_then(|registry| {
            registry
                .into_iter()
                .find(|entry| entry.pid == options.canister)
                .and_then(|entry| entry.role)
        });
        return Ok(EndpointTarget {
            canister: options.canister.clone(),
            role,
        });
    }

    let registry = load_fleet_registry(options)?;
    let entry = registry
        .iter()
        .find(|entry| entry.role.as_deref() == Some(options.canister.as_str()))
        .ok_or_else(|| -> Box<dyn std::error::Error> {
            format!(
                "role {} was not found in fleet {}",
                options.canister, options.fleet
            )
            .into()
        })?;
    Ok(EndpointTarget {
        canister: entry.pid.clone(),
        role: entry.role.clone(),
    })
}

fn load_fleet_registry(
    options: &EndpointsOptions,
) -> Result<Vec<RegistryEntry>, Box<dyn std::error::Error>> {
    let request = InstalledFleetRequest {
        fleet: options.fleet.clone(),
        network: state_network(options),
        icp: options.icp.clone(),
        detect_lost_local_root: false,
    };
    let root = resolve_endpoint_icp_root()?;
    Ok(resolve_installed_fleet_from_root(&request, &root)?
        .registry
        .entries)
}

fn resolve_role_did(
    options: &EndpointsOptions,
    role: &str,
) -> Result<PathBuf, EndpointsCommandError> {
    let root = resolve_endpoint_icp_root().unwrap_or_else(|_| PathBuf::from("."));
    for network in artifact_network_candidates(options) {
        let path = root
            .join(".icp")
            .join(&network)
            .join("canisters")
            .join(role)
            .join(format!("{role}.did"));
        if path.is_file() {
            return Ok(path);
        }
    }

    Err(EndpointsCommandError::MissingRoleArtifact {
        role: role.to_string(),
        root: root.display().to_string(),
    })
}

fn resolve_endpoint_icp_root() -> Result<PathBuf, Box<dyn std::error::Error>> {
    resolve_current_canic_icp_root(None).map_err(Into::into)
}

fn artifact_network_candidates(options: &EndpointsOptions) -> Vec<String> {
    let mut networks = Vec::new();
    if let Some(network) = &options.network {
        networks.push(network.clone());
    }
    networks.push(local_network());
    networks.sort();
    networks.dedup();
    networks
}

fn state_network(options: &EndpointsOptions) -> String {
    options.network.clone().unwrap_or_else(local_network)
}

fn read_did(path: &Path) -> Result<String, EndpointsCommandError> {
    fs::read_to_string(path).map_err(|source| EndpointsCommandError::ReadDid {
        path: path.display().to_string(),
        source,
    })
}

fn is_principal_like(value: &str) -> bool {
    value.contains('-')
        && value
            .chars()
            .all(|ch| ch.is_ascii_alphanumeric() || ch == '-')
}