1use crate::types::Cycles;
8use crate::{
9 Error,
10 cdk::{api::canister_self, mgmt::CanisterInstallMode},
11 config::Config,
12 interface::{
13 ic::{
14 delete_canister as mgmt_delete_canister, deposit_cycles, get_cycles, install_code,
15 uninstall_code,
16 },
17 prelude::*,
18 },
19 log::Topic,
20 ops::{
21 CanisterInitPayload, OpsError,
22 config::ConfigOps,
23 model::memory::{
24 EnvOps,
25 directory::{AppDirectoryOps, SubnetDirectoryOps},
26 env::EnvData,
27 reserve::CanisterReserveOps,
28 topology::SubnetCanisterRegistryOps,
29 },
30 sync::state::StateBundle,
31 wasm::WasmOps,
32 },
33};
34use candid::Principal;
35use thiserror::Error as ThisError;
36
37#[derive(Debug, ThisError)]
38pub enum ProvisioningError {
39 #[error(transparent)]
40 Other(#[from] Error),
41
42 #[error("install failed for {pid}: {source}")]
43 InstallFailed { pid: Principal, source: Error },
44}
45
46pub(crate) async fn rebuild_directories_from_registry(
56 updated_ty: Option<&CanisterRole>,
57) -> Result<StateBundle, Error> {
58 let mut bundle = StateBundle::default();
59 let cfg = Config::get();
60
61 let include_app = updated_ty.is_none_or(|ty| cfg.app_directory.contains(ty));
63 let include_subnet = updated_ty.is_none_or(|ty| {
64 ConfigOps::current_subnet()
65 .map(|c| c.subnet_directory.contains(ty))
66 .unwrap_or(true)
68 });
69
70 if include_app {
71 let app_view = AppDirectoryOps::root_build_view();
72 AppDirectoryOps::import(app_view.clone());
73 bundle.app_directory = Some(app_view);
74 }
75
76 if include_subnet {
77 let subnet_view = SubnetDirectoryOps::root_build_view();
78 SubnetDirectoryOps::import(subnet_view.clone());
79 bundle.subnet_directory = Some(subnet_view);
80 }
81
82 Ok(bundle)
83}
84
85pub async fn create_and_install_canister(
99 ty: &CanisterRole,
100 parent_pid: Principal,
101 extra_arg: Option<Vec<u8>>,
102) -> Result<Principal, ProvisioningError> {
103 WasmOps::try_get(ty)?;
105
106 let pid = allocate_canister(ty).await?;
108
109 if let Err(err) = install_canister(pid, ty, parent_pid, extra_arg).await {
111 return Err(ProvisioningError::InstallFailed { pid, source: err });
112 }
113
114 Ok(pid)
115}
116
117pub async fn delete_canister(
132 pid: Principal,
133) -> Result<(Option<CanisterRole>, Option<Principal>), Error> {
134 OpsError::require_root()?;
135 let parent_pid = SubnetCanisterRegistryOps::get_parent(pid);
136
137 uninstall_code(pid).await?;
139
140 mgmt_delete_canister(pid).await?;
142
143 let removed_entry = SubnetCanisterRegistryOps::remove(&pid);
145 match &removed_entry {
146 Some(c) => log!(
147 Topic::CanisterLifecycle,
148 Ok,
149 "🗑️ delete_canister: {} ({})",
150 pid,
151 c.ty
152 ),
153 None => log!(
154 Topic::CanisterLifecycle,
155 Warn,
156 "🗑️ delete_canister: {pid} not in registry"
157 ),
158 }
159
160 Ok((removed_entry.map(|e| e.ty), parent_pid))
161}
162
163pub async fn uninstall_canister(pid: Principal) -> Result<(), Error> {
165 uninstall_code(pid).await?;
166
167 log!(Topic::CanisterLifecycle, Ok, "🗑️ uninstall_canister: {pid}");
168
169 Ok(())
170}
171
172pub async fn allocate_canister(ty: &CanisterRole) -> Result<Principal, Error> {
182 let cfg = ConfigOps::current_subnet_canister(ty)?;
184
185 let target = cfg.initial_cycles;
186
187 if let Some((pid, _)) = CanisterReserveOps::pop_first() {
189 let mut current = get_cycles(pid).await?;
190
191 if current < target {
192 let missing = target.to_u128().saturating_sub(current.to_u128());
193 if missing > 0 {
194 deposit_cycles(pid, missing).await?;
195 current = Cycles::new(current.to_u128() + missing);
196
197 log!(
198 Topic::CanisterReserve,
199 Ok,
200 "⚡ allocate_canister: topped up {pid} by {} to meet target {}",
201 Cycles::from(missing),
202 target
203 );
204 }
205 }
206
207 log!(
208 Topic::CanisterReserve,
209 Ok,
210 "⚡ allocate_canister: reusing {pid} from pool (current {current})"
211 );
212
213 return Ok(pid);
214 }
215
216 let pid = create_canister(target).await?;
218 log!(
219 Topic::CanisterReserve,
220 Info,
221 "⚡ allocate_canister: pool empty"
222 );
223
224 Ok(pid)
225}
226
227pub(crate) async fn create_canister(cycles: Cycles) -> Result<Principal, Error> {
229 let mut controllers = Config::get().controllers.clone();
230 controllers.push(canister_self()); let pid = crate::interface::ic::canister::create_canister(controllers, cycles.clone()).await?;
233
234 log!(
235 Topic::CanisterLifecycle,
236 Ok,
237 "⚡ create_canister: {pid} ({cycles})"
238 );
239
240 Ok(pid)
241}
242
243#[allow(clippy::cast_precision_loss)]
251async fn install_canister(
252 pid: Principal,
253 ty: &CanisterRole,
254 parent_pid: Principal,
255 extra_arg: Option<Vec<u8>>,
256) -> Result<(), Error> {
257 let wasm = WasmOps::try_get(ty)?;
259
260 let env = EnvData {
262 prime_root_pid: Some(EnvOps::try_get_prime_root_pid()?),
263 subnet_type: Some(EnvOps::try_get_subnet_type()?),
264 subnet_pid: Some(EnvOps::try_get_subnet_pid()?),
265 root_pid: Some(EnvOps::try_get_root_pid()?),
266 canister_type: Some(ty.clone()),
267 parent_pid: Some(parent_pid),
268 };
269
270 let payload = CanisterInitPayload {
271 env,
272 app_directory: AppDirectoryOps::export(),
273 subnet_directory: SubnetDirectoryOps::export(),
274 };
275
276 let module_hash = wasm.module_hash();
277
278 SubnetCanisterRegistryOps::register(pid, ty, parent_pid, module_hash.clone());
281
282 if let Err(err) = install_code(
283 CanisterInstallMode::Install,
284 pid,
285 wasm.bytes(),
286 (payload, extra_arg),
287 )
288 .await
289 {
290 let removed = SubnetCanisterRegistryOps::remove(&pid);
291 if removed.is_none() {
292 log!(
293 Topic::CanisterLifecycle,
294 Warn,
295 "⚠️ install_canister rollback: {pid} missing from registry after failed install"
296 );
297 }
298
299 return Err(err);
300 }
301
302 log!(
303 Topic::CanisterLifecycle,
304 Ok,
305 "⚡ install_canister: {pid} ({ty}, {:.2} KiB)",
306 wasm.len() as f64 / 1_024.0,
307 );
308
309 Ok(())
310}
311
312