use crate::{
InternalError, InternalErrorOrigin,
api::runtime::install::{ApprovedModuleSource, ModuleSourceRuntimeApi},
config::Config,
config::schema::CanisterKind,
domain::policy,
dto::{abi::v1::CanisterInitPayload, env::EnvBootstrapArgs},
ops::{
config::ConfigOps,
ic::{
IcOps,
mgmt::{CanisterInstallMode, MgmtOps},
},
runtime::env::EnvOps,
storage::{
directory::{app::AppDirectoryOps, subnet::SubnetDirectoryOps},
registry::subnet::SubnetRegistryOps,
},
topology::directory::builder::{RootAppDirectoryBuilder, RootSubnetDirectoryBuilder},
},
workflow::{
cascade::snapshot::StateSnapshotBuilder, pool::PoolWorkflow, prelude::*,
runtime::install::ModuleInstallWorkflow,
},
};
pub struct ProvisionWorkflow;
impl ProvisionWorkflow {
pub fn build_nonroot_init_payload(
role: &CanisterRole,
parent_pid: Principal,
) -> Result<CanisterInitPayload, InternalError> {
let env = EnvBootstrapArgs {
prime_root_pid: Some(EnvOps::prime_root_pid()?),
subnet_role: Some(EnvOps::subnet_role()?),
subnet_pid: Some(EnvOps::subnet_pid()?),
root_pid: Some(EnvOps::root_pid()?),
canister_role: Some(role.clone()),
parent_pid: Some(parent_pid),
};
let app_directory = AppDirectoryOps::snapshot_args();
let subnet_directory = SubnetDirectoryOps::snapshot_args();
Ok(CanisterInitPayload {
env,
app_directory,
subnet_directory,
})
}
pub fn rebuild_directories_from_registry(
updated_role: Option<&CanisterRole>,
) -> Result<StateSnapshotBuilder, InternalError> {
let cfg = ConfigOps::get()?;
let subnet_cfg = ConfigOps::current_subnet()?;
let registry = SubnetRegistryOps::data();
let allow_incomplete = updated_role.is_some();
let include_app = updated_role.is_none_or(|role| cfg.app_directory.contains(role));
let include_subnet =
updated_role.is_none_or(|role| subnet_cfg.subnet_directory.contains(role));
let mut builder = StateSnapshotBuilder::new()?;
if include_app {
let app_data = RootAppDirectoryBuilder::build(®istry, &cfg.app_directory)?;
if allow_incomplete {
AppDirectoryOps::import_allow_incomplete(app_data)?;
} else {
AppDirectoryOps::import(app_data)?;
}
builder = builder.with_app_directory()?;
}
if include_subnet {
let subnet_data =
RootSubnetDirectoryBuilder::build(®istry, &subnet_cfg.subnet_directory)?;
if allow_incomplete {
SubnetDirectoryOps::import_allow_incomplete(subnet_data)?;
} else {
SubnetDirectoryOps::import(subnet_data)?;
}
builder = builder.with_subnet_directory()?;
}
Ok(builder)
}
pub async fn create_and_install_canister(
role: &CanisterRole,
parent_pid: Principal,
extra_arg: Option<Vec<u8>>,
) -> Result<Principal, InternalError> {
let module_source = ModuleSourceRuntimeApi::approved_module_source(role).await?;
let (pid, source) = allocate_canister(role).await?;
if let Err(err) = install_canister(pid, role, parent_pid, &module_source, extra_arg).await {
log!(
Topic::CanisterLifecycle,
Error,
"install failed for {pid} ({role}): {err}"
);
if source == AllocationSource::Pool {
if let Err(recycle_err) = PoolWorkflow::pool_import_canister(pid).await {
log!(
Topic::CanisterPool,
Warn,
"failed to recycle pool canister after install failure: {pid} ({recycle_err})"
);
}
} else if let Err(delete_err) = Self::uninstall_and_delete_canister(pid).await {
log!(
Topic::CanisterLifecycle,
Warn,
"failed to delete canister after install failure: {pid} ({delete_err})"
);
}
return Err(InternalError::workflow(
InternalErrorOrigin::Workflow,
format!("failed to install canister {pid}: {err}"),
));
}
Ok(pid)
}
pub async fn uninstall_and_delete_canister(pid: Principal) -> Result<(), InternalError> {
EnvOps::require_root()?;
MgmtOps::uninstall_code(pid).await?;
MgmtOps::stop_canister(pid).await?;
MgmtOps::delete_canister(pid).await?;
let removed_entry = SubnetRegistryOps::remove(&pid);
match &removed_entry {
Some(c) => log!(
Topic::CanisterLifecycle,
Ok,
"🗑️ delete_canister: {} ({})",
pid,
c.role
),
None => log!(
Topic::CanisterLifecycle,
Warn,
"🗑️ delete_canister: {pid} not in registry"
),
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum AllocationSource {
Pool,
New,
}
async fn allocate_canister(
role: &CanisterRole,
) -> Result<(Principal, AllocationSource), InternalError> {
let cfg = ConfigOps::current_subnet_canister(role)?;
let target = cfg.initial_cycles;
if let Some(pid) = PoolWorkflow::pop_oldest_ready() {
let mut current = MgmtOps::get_cycles(pid).await?;
if current < target {
let missing = target.to_u128().saturating_sub(current.to_u128());
if missing > 0 {
MgmtOps::deposit_cycles(pid, missing).await?;
current = Cycles::new(current.to_u128() + missing);
log!(
Topic::CanisterPool,
Ok,
"⚡ allocate_canister: topped up {pid} by {} to meet target {}",
Cycles::from(missing),
target
);
}
}
log!(
Topic::CanisterPool,
Ok,
"⚡ allocate_canister: reusing {pid} role={role} from pool (current {current})"
);
return Ok((pid, AllocationSource::Pool));
}
let pid = create_canister_with_configured_controllers(role, target).await?;
Ok((pid, AllocationSource::New))
}
async fn create_canister_with_configured_controllers(
role: &CanisterRole,
cycles: Cycles,
) -> Result<Principal, InternalError> {
let root = IcOps::canister_self();
let mut controllers = Config::get()?.controllers.clone();
controllers.push(root);
let pid = MgmtOps::create_canister(controllers, cycles.clone()).await?;
log!(
Topic::CanisterLifecycle,
Ok,
"⚡ create_canister: {pid} role={role} cycles={cycles} source=new (pool empty)"
);
Ok(pid)
}
async fn install_canister(
pid: Principal,
role: &CanisterRole,
parent_pid: Principal,
module_source: &ApprovedModuleSource,
extra_arg: Option<Vec<u8>>,
) -> Result<(), InternalError> {
let payload = ProvisionWorkflow::build_nonroot_init_payload(role, parent_pid)?;
let module_hash = module_source.module_hash().to_vec();
validate_registration_policy(role, parent_pid)?;
let created_at = IcOps::now_secs();
SubnetRegistryOps::register_unchecked(pid, role, parent_pid, module_hash.clone(), created_at)?;
if let Err(err) = ModuleInstallWorkflow::install_with_payload(
CanisterInstallMode::Install,
pid,
module_source,
payload,
extra_arg,
)
.await
{
let removed = SubnetRegistryOps::remove(&pid);
if removed.is_none() {
log!(
Topic::CanisterLifecycle,
Warn,
"⚠️ install_canister rollback: {pid} missing from registry after failed install"
);
}
return Err(err);
}
log!(
Topic::CanisterLifecycle,
Ok,
"⚡ install_canister: {pid} ({role}, source={}, size={}, chunks={})",
module_source.source_label(),
module_source.payload_size(),
module_source.chunk_count(),
);
Ok(())
}
fn validate_registration_policy(
role: &CanisterRole,
parent_pid: Principal,
) -> Result<(), InternalError> {
let canister_cfg = ConfigOps::current_subnet_canister(role)?;
let parent_role = SubnetRegistryOps::get(parent_pid)
.map(|record| record.role)
.ok_or(policy::topology::TopologyPolicyError::ParentNotFound(
parent_pid,
))?;
let parent_cfg = ConfigOps::current_subnet_canister(&parent_role)?;
let observed = policy::topology::registry::RegistryRegistrationObservation {
existing_role_pid: matches!(canister_cfg.kind, CanisterKind::Root)
.then(|| SubnetRegistryOps::find_pid_for_role(role))
.flatten(),
existing_singleton_under_parent_pid: matches!(canister_cfg.kind, CanisterKind::Singleton)
.then(|| {
if role.is_wasm_store() {
None
} else {
SubnetRegistryOps::find_child_pid_for_role(parent_pid, role)
}
})
.flatten(),
};
policy::topology::registry::RegistryPolicy::can_register_role_observed(
role,
parent_pid,
observed,
&canister_cfg,
&parent_role,
&parent_cfg,
)
.map_err(policy::topology::TopologyPolicyError::from)?;
Ok(())
}