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 prelude::*,
22 reserve::ReserveOps,
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) -> Result<CanisterInitPayload, Error> {
40 let env = EnvData {
41 prime_root_pid: Some(EnvOps::try_get_prime_root_pid()?),
42 subnet_role: Some(EnvOps::try_get_subnet_role()?),
43 subnet_pid: Some(EnvOps::try_get_subnet_pid()?),
44 root_pid: Some(EnvOps::try_get_root_pid()?),
45 canister_role: Some(role.clone()),
46 parent_pid: Some(parent_pid),
47 };
48
49 Ok(CanisterInitPayload {
50 env,
51 app_directory: AppDirectoryOps::export(),
52 subnet_directory: SubnetDirectoryOps::export(),
53 })
54}
55
56#[derive(Debug, ThisError)]
61pub enum ProvisionOpsError {
62 #[error(transparent)]
63 Other(#[from] Error),
64
65 #[error("install failed for {pid}: {source}")]
66 InstallFailed { pid: Principal, source: Error },
67}
68
69impl From<ProvisionOpsError> for Error {
70 fn from(err: ProvisionOpsError) -> Self {
71 IcOpsError::from(err).into()
72 }
73}
74
75pub(crate) async fn rebuild_directories_from_registry(
85 updated_role: Option<&CanisterRole>,
86) -> Result<StateBundle, Error> {
87 let mut bundle = StateBundle::default();
88 let cfg = Config::get();
89
90 let include_app = updated_role.is_none_or(|role| cfg.app_directory.contains(role));
92 let include_subnet = updated_role.is_none_or(|role| {
93 ConfigOps::current_subnet()
94 .map(|c| c.subnet_directory.contains(role))
95 .unwrap_or(true)
97 });
98
99 if include_app {
100 let app_view = AppDirectoryOps::root_build_view();
101 AppDirectoryOps::import(app_view.clone());
102 bundle.app_directory = Some(app_view);
103 }
104
105 if include_subnet {
106 let subnet_view = SubnetDirectoryOps::root_build_view();
107 SubnetDirectoryOps::import(subnet_view.clone());
108 bundle.subnet_directory = Some(subnet_view);
109 }
110
111 Ok(bundle)
112}
113
114pub async fn create_and_install_canister(
128 role: &CanisterRole,
129 parent_pid: Principal,
130 extra_arg: Option<Vec<u8>>,
131) -> Result<Principal, Error> {
132 WasmOps::try_get(role)?;
134
135 let pid = allocate_canister(role).await?;
137
138 if let Err(err) = install_canister(pid, role, parent_pid, extra_arg).await {
140 return Err(ProvisionOpsError::InstallFailed { pid, source: err }.into());
141 }
142
143 Ok(pid)
144}
145
146pub async fn delete_canister(pid: Principal) -> Result<(), Error> {
161 OpsError::require_root()?;
162
163 super::uninstall_code(pid).await?;
165
166 super::delete_canister(pid).await?;
168
169 let removed_entry = SubnetCanisterRegistryOps::remove(&pid);
171 match &removed_entry {
172 Some(c) => log!(
173 Topic::CanisterLifecycle,
174 Ok,
175 "🗑️ delete_canister: {} ({})",
176 pid,
177 c.role
178 ),
179 None => log!(
180 Topic::CanisterLifecycle,
181 Warn,
182 "🗑️ delete_canister: {pid} not in registry"
183 ),
184 }
185
186 Ok(())
187}
188
189pub async fn uninstall_canister(pid: Principal) -> Result<(), Error> {
191 super::uninstall_code(pid).await?;
192
193 log!(Topic::CanisterLifecycle, Ok, "🗑️ uninstall_canister: {pid}");
194
195 Ok(())
196}
197
198pub async fn allocate_canister(role: &CanisterRole) -> Result<Principal, Error> {
208 let cfg = ConfigOps::current_subnet_canister(role)?;
210 let target = cfg.initial_cycles;
211
212 if let Some((pid, _)) = ReserveOps::pop_first() {
214 let mut current = super::get_cycles(pid).await?;
215
216 if current < target {
217 let missing = target.to_u128().saturating_sub(current.to_u128());
218 if missing > 0 {
219 super::deposit_cycles(pid, missing).await?;
220 current = Cycles::new(current.to_u128() + missing);
221
222 log!(
223 Topic::CanisterReserve,
224 Ok,
225 "⚡ allocate_canister: topped up {pid} by {} to meet target {}",
226 Cycles::from(missing),
227 target
228 );
229 }
230 }
231
232 log!(
233 Topic::CanisterReserve,
234 Ok,
235 "⚡ allocate_canister: reusing {pid} from pool (current {current})"
236 );
237
238 return Ok(pid);
239 }
240
241 let pid = create_canister_with_configured_controllers(target).await?;
243 log!(
244 Topic::CanisterReserve,
245 Info,
246 "⚡ allocate_canister: pool empty"
247 );
248
249 Ok(pid)
250}
251
252async fn create_canister_with_configured_controllers(cycles: Cycles) -> Result<Principal, Error> {
254 let mut controllers = Config::get().controllers.clone();
255 controllers.push(canister_self()); let pid = super::create_canister(controllers, cycles.clone()).await?;
258
259 log!(
260 Topic::CanisterLifecycle,
261 Ok,
262 "⚡ create_canister: {pid} ({cycles})"
263 );
264
265 Ok(pid)
266}
267
268#[allow(clippy::cast_precision_loss)]
276async fn install_canister(
277 pid: Principal,
278 role: &CanisterRole,
279 parent_pid: Principal,
280 extra_arg: Option<Vec<u8>>,
281) -> Result<(), Error> {
282 let wasm = WasmOps::try_get(role)?;
284
285 let payload = build_nonroot_init_payload(role, parent_pid)?;
286
287 let module_hash = wasm.module_hash();
288
289 SubnetCanisterRegistryOps::register(pid, role, parent_pid, module_hash.clone());
292
293 if let Err(err) = super::install_canic_code(
294 CanisterInstallMode::Install,
295 pid,
296 wasm.bytes(),
297 payload,
298 extra_arg,
299 )
300 .await
301 {
302 let removed = SubnetCanisterRegistryOps::remove(&pid);
303 if removed.is_none() {
304 log!(
305 Topic::CanisterLifecycle,
306 Warn,
307 "⚠️ install_canister rollback: {pid} missing from registry after failed install"
308 );
309 }
310
311 return Err(err);
312 }
313
314 log!(
315 Topic::CanisterLifecycle,
316 Ok,
317 "⚡ install_canister: {pid} ({role}, {:.2} KiB)",
318 wasm.len() as f64 / 1_024.0,
319 );
320
321 Ok(())
322}