canic-cli 0.95.8

Operator CLI for Canic fleet setup, builds, evidence, catalog, backup, and restore workflows
Documentation
use crate::{
    cli::defaults::local_environment,
    endpoints::{
        CANDID_SERVICE_METADATA, EndpointsCommandError, EndpointsOptions,
        model::{EndpointReport, EndpointTarget},
    },
};
use canic_host::{
    candid_endpoints::parse_candid_service_endpoints,
    icp::{IcpCli, local_canister_candid_path},
    icp_config::resolve_current_canic_icp_root,
    installed_deployment::{InstalledDeploymentRequest, resolve_installed_deployment_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 {
            deployment: options.deployment.clone(),
            canister: options.canister.clone(),
        });
    };
    let path = resolve_role_did(&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, options.environment.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 deployment target {}",
                options.canister, options.deployment
            )
            .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 = InstalledDeploymentRequest {
        deployment: options.deployment.clone(),
        environment: state_environment(options),
        icp: options.icp.clone(),
        detect_lost_local_root: false,
    };
    let root = resolve_endpoint_icp_root()?;
    Ok(resolve_installed_deployment_from_root(&request, &root)?
        .registry
        .entries)
}

fn resolve_role_did(role: &str) -> Result<PathBuf, EndpointsCommandError> {
    let root = resolve_current_canic_icp_root()?;
    let path = local_canister_candid_path(&root, &local_environment(), role);
    if path.is_file() {
        return Ok(path);
    }

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

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

fn state_environment(options: &EndpointsOptions) -> String {
    options
        .environment
        .clone()
        .unwrap_or_else(local_environment)
}

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 == '-')
}