1use crate::{
12 Error,
13 cdk::mgmt::CanisterInstallMode,
14 config::Config,
15 dto::{abi::v1::CanisterInitPayload, env::EnvView},
16 ops::{
17 adapter::directory::{app_directory_to_view, subnet_directory_to_view},
18 config::ConfigOps,
19 ic::{create_canister, delete_canister, deposit_cycles, get_cycles, uninstall_code},
20 runtime::{canister::install_code_with_extra_arg, env::EnvOps, wasm::WasmOps},
21 storage::{
22 directory::{AppDirectoryOps, SubnetDirectoryOps},
23 pool::PoolOps,
24 registry::SubnetRegistryOps,
25 },
26 },
27 workflow::{
28 directory::{RootAppDirectoryBuilder, RootSubnetDirectoryBuilder},
29 ic::IcError,
30 pool::pool_import_canister,
31 prelude::*,
32 snapshot::StateSnapshotBuilder,
33 },
34};
35use thiserror::Error as ThisError;
36
37pub(crate) fn build_nonroot_init_payload(
38 role: &CanisterRole,
39 parent_pid: Principal,
40) -> Result<CanisterInitPayload, Error> {
41 let env = EnvView {
42 prime_root_pid: Some(EnvOps::prime_root_pid()?),
43 subnet_role: Some(EnvOps::subnet_role()?),
44 subnet_pid: Some(EnvOps::subnet_pid()?),
45 root_pid: Some(EnvOps::root_pid()?),
46 canister_role: Some(role.clone()),
47 parent_pid: Some(parent_pid),
48 };
49
50 Ok(CanisterInitPayload::new(
51 env,
52 app_directory_to_view(AppDirectoryOps::export()),
53 subnet_directory_to_view(SubnetDirectoryOps::export()),
54 ))
55}
56
57#[derive(Debug, ThisError)]
62pub enum ProvisionError {
63 #[error("install failed for {pid}: {source}")]
64 InstallFailed { pid: Principal, source: Error },
65}
66
67impl From<ProvisionError> for Error {
68 fn from(err: ProvisionError) -> Self {
69 IcError::from(err).into()
70 }
71}
72
73pub(crate) async fn rebuild_directories_from_registry(
84 updated_role: Option<&CanisterRole>,
85) -> Result<StateSnapshotBuilder, Error> {
86 let cfg = Config::get()?;
87 let subnet_cfg = ConfigOps::current_subnet()?;
88
89 let include_app = updated_role.is_none_or(|role| cfg.app_directory.contains(role));
90 let include_subnet = updated_role.is_none_or(|role| subnet_cfg.subnet_directory.contains(role));
91
92 let mut builder = StateSnapshotBuilder::new();
93
94 if include_app {
95 let data = RootAppDirectoryBuilder::build_from_registry();
96 AppDirectoryOps::import(data.clone());
97 builder = builder.with_app_directory_view(app_directory_to_view(data));
98 }
99
100 if include_subnet {
101 let data = RootSubnetDirectoryBuilder::build_from_registry();
102 SubnetDirectoryOps::import(data.clone());
103 builder = builder.with_subnet_directory_view(subnet_directory_to_view(data));
104 }
105
106 Ok(builder)
107}
108
109pub async fn create_and_install_canister(
123 role: &CanisterRole,
124 parent_pid: Principal,
125 extra_arg: Option<Vec<u8>>,
126) -> Result<Principal, Error> {
127 WasmOps::try_get(role)?;
129
130 let (pid, source) = allocate_canister_with_source(role).await?;
132
133 if let Err(err) = install_canister(pid, role, parent_pid, extra_arg).await {
135 if source == AllocationSource::Pool {
136 if let Err(recycle_err) = pool_import_canister(pid).await {
137 log!(
138 Topic::CanisterPool,
139 Warn,
140 "failed to recycle pool canister after install failure: {pid} ({recycle_err})"
141 );
142 }
143 } else if let Err(delete_err) = uninstall_and_delete_canister(pid).await {
144 log!(
145 Topic::CanisterLifecycle,
146 Warn,
147 "failed to delete canister after install failure: {pid} ({delete_err})"
148 );
149 }
150 return Err(ProvisionError::InstallFailed { pid, source: err }.into());
151 }
152
153 Ok(pid)
154}
155
156pub async fn uninstall_and_delete_canister(pid: Principal) -> Result<(), Error> {
171 EnvOps::require_root()?;
172
173 uninstall_code(pid).await?;
175
176 delete_canister(pid).await?;
178
179 let removed_entry = SubnetRegistryOps::remove(&pid);
181 match &removed_entry {
182 Some(c) => log!(
183 Topic::CanisterLifecycle,
184 Ok,
185 "🗑️ delete_canister: {} ({})",
186 pid,
187 c.role
188 ),
189 None => log!(
190 Topic::CanisterLifecycle,
191 Warn,
192 "🗑️ delete_canister: {pid} not in registry"
193 ),
194 }
195
196 Ok(())
197}
198
199pub async fn allocate_canister(role: &CanisterRole) -> Result<Principal, Error> {
209 let (pid, _) = allocate_canister_with_source(role).await?;
210 Ok(pid)
211}
212
213#[derive(Clone, Copy, Debug, Eq, PartialEq)]
214enum AllocationSource {
215 Pool,
216 New,
217}
218
219async fn allocate_canister_with_source(
220 role: &CanisterRole,
221) -> Result<(Principal, AllocationSource), Error> {
222 let cfg = ConfigOps::current_subnet_canister(role)?;
224 let target = cfg.initial_cycles;
225
226 if let Some((pid, _)) = PoolOps::pop_ready() {
228 let mut current = get_cycles(pid).await?;
229
230 if current < target {
231 let missing = target.to_u128().saturating_sub(current.to_u128());
232 if missing > 0 {
233 deposit_cycles(pid, missing).await?;
234 current = Cycles::new(current.to_u128() + missing);
235
236 log!(
237 Topic::CanisterPool,
238 Ok,
239 "⚡ allocate_canister: topped up {pid} by {} to meet target {}",
240 Cycles::from(missing),
241 target
242 );
243 }
244 }
245
246 log!(
247 Topic::CanisterPool,
248 Ok,
249 "⚡ allocate_canister: reusing {pid} from pool (current {current})"
250 );
251
252 return Ok((pid, AllocationSource::Pool));
253 }
254
255 let pid = create_canister_with_configured_controllers(target).await?;
257 log!(
258 Topic::CanisterPool,
259 Info,
260 "⚡ allocate_canister: pool empty"
261 );
262
263 Ok((pid, AllocationSource::New))
264}
265
266async fn create_canister_with_configured_controllers(cycles: Cycles) -> Result<Principal, Error> {
268 let root = canister_self();
269 let mut controllers = Config::get()?.controllers.clone();
270 controllers.push(root); let pid = create_canister(controllers, cycles.clone()).await?;
273
274 log!(
275 Topic::CanisterLifecycle,
276 Ok,
277 "⚡ create_canister: {pid} ({cycles})"
278 );
279
280 Ok(pid)
281}
282
283#[allow(clippy::cast_precision_loss)]
291async fn install_canister(
292 pid: Principal,
293 role: &CanisterRole,
294 parent_pid: Principal,
295 extra_arg: Option<Vec<u8>>,
296) -> Result<(), Error> {
297 let wasm = WasmOps::try_get(role)?;
299
300 let payload = build_nonroot_init_payload(role, parent_pid)?;
301 let module_hash = wasm.module_hash();
302
303 SubnetRegistryOps::register(pid, role, parent_pid, module_hash.clone())?;
306
307 if let Err(err) = install_code_with_extra_arg(
308 CanisterInstallMode::Install,
309 pid,
310 wasm.bytes(),
311 payload,
312 extra_arg,
313 )
314 .await
315 {
316 let removed = SubnetRegistryOps::remove(&pid);
317 if removed.is_none() {
318 log!(
319 Topic::CanisterLifecycle,
320 Warn,
321 "⚠️ install_canister rollback: {pid} missing from registry after failed install"
322 );
323 }
324
325 return Err(err);
326 }
327
328 log!(
329 Topic::CanisterLifecycle,
330 Ok,
331 "⚡ install_canister: {pid} ({role}, {:.2} KiB)",
332 wasm.len() as f64 / 1_024.0,
333 );
334
335 Ok(())
336}