canic-control-plane 0.79.4

Canic — a canister orchestration and management toolkit for the Internet Computer
Documentation
use crate::ids::CanisterRole;
use canic_core::{
    control_plane_support::{
        cdk::types::{Principal, TC},
        error::{InternalError, InternalErrorOrigin},
        ops::{
            config::ConfigOps,
            cost_guard::{
                CostGuardOps, CostGuardPermit, CostGuardRequest, CostGuardReserveError,
                CostGuardReservePublicKind,
            },
            ic::{IcOps, mgmt::MgmtOps},
            replay::model::CommandKind,
        },
        replay_policy::CostClass,
        workflow::canister_lifecycle::{
            CanisterLifecycleEvent, CanisterLifecycleResult, CanisterLifecycleWorkflow,
        },
    },
    dto::error::Error,
    log,
    log::Topic,
};

const CONTROL_PLANE_DEPLOYMENT_QUOTA_WINDOW_SECONDS: u64 = 60;
const MAX_CONTROL_PLANE_DEPLOYMENT_OPERATIONS_PER_WINDOW: u64 = 64;
const MIN_CONTROL_PLANE_CYCLES_AFTER_RESERVATION: u128 = TC;

pub const BOOTSTRAP_AUTO_CREATE_COMMAND_KIND: &str =
    "management.control_plane.bootstrap_auto_create.v1";
pub const BOOTSTRAP_WASM_STORE_CREATE_COMMAND_KIND: &str =
    "management.control_plane.bootstrap_wasm_store_create.v1";
pub const PUBLICATION_WASM_STORE_CREATE_COMMAND_KIND: &str =
    "management.control_plane.publication_wasm_store_create.v1";

pub async fn create_canister_with_deployment_guard(
    command_kind: &'static str,
    role: CanisterRole,
    parent: Principal,
    extra_arg: Option<Vec<u8>>,
) -> Result<CanisterLifecycleResult, InternalError> {
    let quota_subject = IcOps::canister_self();
    let payer = IcOps::canister_self();
    let cost_permit =
        reserve_control_plane_deployment_cost_guard(command_kind, &role, quota_subject, payer)?;
    log!(
        Topic::CanisterLifecycle,
        Info,
        "control_plane_create: deployment cost guard reserved command_kind={} role={} parent={} quota_subject={} payer={}",
        command_kind,
        role,
        parent,
        quota_subject,
        payer
    );

    let result = CanisterLifecycleWorkflow::apply(CanisterLifecycleEvent::Create {
        deployment_permit: &cost_permit,
        role,
        parent,
        extra_arg,
    })
    .await;

    match result {
        Ok(result) => {
            CostGuardOps::complete(&cost_permit, IcOps::now_secs())?;
            Ok(result)
        }
        Err(err) => {
            let _ = CostGuardOps::recover(&cost_permit, IcOps::now_secs());
            Err(err)
        }
    }
}

fn reserve_control_plane_deployment_cost_guard(
    command_kind: &'static str,
    role: &CanisterRole,
    quota_subject: Principal,
    payer: Principal,
) -> Result<CostGuardPermit, InternalError> {
    let cycle_reservation_cycles = ConfigOps::current_subnet()?
        .get_canister(role)
        .ok_or_else(|| {
            InternalError::workflow(
                InternalErrorOrigin::Config,
                format!("canister {role} not defined in current subnet"),
            )
        })?
        .initial_cycles
        .to_u128();

    CostGuardOps::reserve(CostGuardRequest {
        cost_class: CostClass::ManagementDeployment,
        command_kind: CommandKind::new(command_kind)
            .expect("control-plane deployment command kind is a valid static label"),
        quota_subject,
        payer,
        now_secs: IcOps::now_secs(),
        quota_window_secs: CONTROL_PLANE_DEPLOYMENT_QUOTA_WINDOW_SECONDS,
        max_operations_per_window: MAX_CONTROL_PLANE_DEPLOYMENT_OPERATIONS_PER_WINDOW,
        current_cycle_balance: MgmtOps::canister_cycle_balance().to_u128(),
        cycle_reservation_cycles,
        min_cycles_after_reservation: MIN_CONTROL_PLANE_CYCLES_AFTER_RESERVATION,
    })
    .map_err(map_control_plane_cost_guard_reserve_error)
}

fn map_control_plane_cost_guard_reserve_error(err: CostGuardReserveError) -> InternalError {
    match err.public_kind() {
        Some(CostGuardReservePublicKind::InvalidInput) => {
            InternalError::public(Error::invalid(err.to_string()))
        }
        Some(CostGuardReservePublicKind::ResourceExhausted) => {
            InternalError::public(Error::exhausted(err.to_string()))
        }
        None => err.into(),
    }
}