1use crate::{
12 Error,
13 cdk::{api::canister_self, mgmt::CanisterInstallMode},
14 config::Config,
15 log::Topic,
16 ops::{
17 OpsError,
18 config::ConfigOps,
19 ic::IcOpsError,
20 orchestration::cascade::state::StateBundle,
21 pool::{PoolOps, pool_import_canister},
22 prelude::*,
23 storage::{
24 CanisterInitPayload,
25 directory::{AppDirectoryOps, SubnetDirectoryOps},
26 env::{EnvData, EnvOps},
27 topology::SubnetCanisterRegistryOps,
28 },
29 wasm::WasmOps,
30 },
31 types::Cycles,
32};
33use candid::Principal;
34use thiserror::Error as ThisError;
35
36pub(crate) fn build_nonroot_init_payload(
37 role: &CanisterRole,
38 parent_pid: Principal,
39) -> CanisterInitPayload {
40 let env = EnvData {
41 prime_root_pid: Some(EnvOps::prime_root_pid()),
42 subnet_role: Some(EnvOps::subnet_role()),
43 subnet_pid: Some(EnvOps::subnet_pid()),
44 root_pid: Some(EnvOps::root_pid()),
45 canister_role: Some(role.clone()),
46 parent_pid: Some(parent_pid),
47 };
48
49 CanisterInitPayload::new(env, AppDirectoryOps::export(), SubnetDirectoryOps::export())
50}
51
52#[derive(Debug, ThisError)]
57pub enum ProvisionOpsError {
58 #[error("install failed for {pid}: {source}")]
59 InstallFailed { pid: Principal, source: Error },
60}
61
62impl From<ProvisionOpsError> for Error {
63 fn from(err: ProvisionOpsError) -> Self {
64 IcOpsError::from(err).into()
65 }
66}
67
68pub(crate) async fn rebuild_directories_from_registry(
80 updated_role: Option<&CanisterRole>,
81) -> StateBundle {
82 let mut bundle = StateBundle::default();
83 let cfg = Config::get();
84
85 let include_app = updated_role.is_none_or(|role| cfg.app_directory.contains(role));
86
87 let include_subnet = updated_role.is_none_or(|role| {
88 let subnet_cfg = ConfigOps::current_subnet();
89 subnet_cfg.subnet_directory.contains(role)
90 });
91
92 if include_app {
93 let app_view = AppDirectoryOps::root_build_view();
94 AppDirectoryOps::import(app_view.clone());
95 bundle.app_directory = Some(app_view);
96 }
97
98 if include_subnet {
99 let subnet_view = SubnetDirectoryOps::root_build_view();
100 SubnetDirectoryOps::import(subnet_view.clone());
101 bundle.subnet_directory = Some(subnet_view);
102 }
103
104 bundle
105}
106
107pub async fn create_and_install_canister(
121 role: &CanisterRole,
122 parent_pid: Principal,
123 extra_arg: Option<Vec<u8>>,
124) -> Result<Principal, Error> {
125 WasmOps::try_get(role)?;
127
128 let (pid, source) = allocate_canister_with_source(role).await?;
130
131 if let Err(err) = install_canister(pid, role, parent_pid, extra_arg).await {
133 if source == AllocationSource::Pool {
134 if let Err(recycle_err) = pool_import_canister(pid).await {
135 log!(
136 Topic::CanisterPool,
137 Warn,
138 "failed to recycle pool canister after install failure: {pid} ({recycle_err})"
139 );
140 }
141 } else if let Err(delete_err) = delete_canister(pid).await {
142 log!(
143 Topic::CanisterLifecycle,
144 Warn,
145 "failed to delete canister after install failure: {pid} ({delete_err})"
146 );
147 }
148 return Err(ProvisionOpsError::InstallFailed { pid, source: err }.into());
149 }
150
151 Ok(pid)
152}
153
154pub async fn delete_canister(pid: Principal) -> Result<(), Error> {
169 OpsError::require_root()?;
170
171 super::uninstall_code(pid).await?;
173
174 super::delete_canister(pid).await?;
176
177 let removed_entry = SubnetCanisterRegistryOps::remove(&pid);
179 match &removed_entry {
180 Some(c) => log!(
181 Topic::CanisterLifecycle,
182 Ok,
183 "🗑️ delete_canister: {} ({})",
184 pid,
185 c.role
186 ),
187 None => log!(
188 Topic::CanisterLifecycle,
189 Warn,
190 "🗑️ delete_canister: {pid} not in registry"
191 ),
192 }
193
194 Ok(())
195}
196
197pub async fn uninstall_canister(pid: Principal) -> Result<(), Error> {
199 super::uninstall_code(pid).await?;
200
201 log!(Topic::CanisterLifecycle, Ok, "🗑️ uninstall_canister: {pid}");
202
203 Ok(())
204}
205
206pub async fn allocate_canister(role: &CanisterRole) -> Result<Principal, Error> {
216 let (pid, _) = allocate_canister_with_source(role).await?;
217 Ok(pid)
218}
219
220#[derive(Clone, Copy, Debug, Eq, PartialEq)]
221enum AllocationSource {
222 Pool,
223 New,
224}
225
226async fn allocate_canister_with_source(
227 role: &CanisterRole,
228) -> Result<(Principal, AllocationSource), Error> {
229 let cfg = ConfigOps::current_subnet_canister(role);
231 let target = cfg.initial_cycles;
232
233 if let Some((pid, _)) = PoolOps::pop_ready() {
235 let mut current = super::get_cycles(pid).await?;
236
237 if current < target {
238 let missing = target.to_u128().saturating_sub(current.to_u128());
239 if missing > 0 {
240 super::deposit_cycles(pid, missing).await?;
241 current = Cycles::new(current.to_u128() + missing);
242
243 log!(
244 Topic::CanisterPool,
245 Ok,
246 "⚡ allocate_canister: topped up {pid} by {} to meet target {}",
247 Cycles::from(missing),
248 target
249 );
250 }
251 }
252
253 log!(
254 Topic::CanisterPool,
255 Ok,
256 "⚡ allocate_canister: reusing {pid} from pool (current {current})"
257 );
258
259 return Ok((pid, AllocationSource::Pool));
260 }
261
262 let pid = create_canister_with_configured_controllers(target).await?;
264 log!(
265 Topic::CanisterPool,
266 Info,
267 "⚡ allocate_canister: pool empty"
268 );
269
270 Ok((pid, AllocationSource::New))
271}
272
273async fn create_canister_with_configured_controllers(cycles: Cycles) -> Result<Principal, Error> {
275 let root = canister_self();
276 let mut controllers = Config::get().controllers.clone();
277 controllers.push(root); let pid = super::create_canister(controllers, cycles.clone()).await?;
280
281 log!(
282 Topic::CanisterLifecycle,
283 Ok,
284 "⚡ create_canister: {pid} ({cycles})"
285 );
286
287 Ok(pid)
288}
289
290#[allow(clippy::cast_precision_loss)]
298async fn install_canister(
299 pid: Principal,
300 role: &CanisterRole,
301 parent_pid: Principal,
302 extra_arg: Option<Vec<u8>>,
303) -> Result<(), Error> {
304 let wasm = WasmOps::try_get(role)?;
306
307 let payload = build_nonroot_init_payload(role, parent_pid);
308 let module_hash = wasm.module_hash();
309
310 SubnetCanisterRegistryOps::register(pid, role, parent_pid, module_hash.clone());
313
314 if let Err(err) = super::install_canic_code(
315 CanisterInstallMode::Install,
316 pid,
317 wasm.bytes(),
318 payload,
319 extra_arg,
320 )
321 .await
322 {
323 let removed = SubnetCanisterRegistryOps::remove(&pid);
324 if removed.is_none() {
325 log!(
326 Topic::CanisterLifecycle,
327 Warn,
328 "⚠️ install_canister rollback: {pid} missing from registry after failed install"
329 );
330 }
331
332 return Err(err);
333 }
334
335 log!(
336 Topic::CanisterLifecycle,
337 Ok,
338 "⚡ install_canister: {pid} ({role}, {:.2} KiB)",
339 wasm.len() as f64 / 1_024.0,
340 );
341
342 Ok(())
343}