1use crate::canister_build::{
2 CanisterBuildProfile, build_current_workspace_canister_artifact,
3 current_workspace_build_context_once,
4};
5use crate::deployment_truth::{
6 ArtifactPromotionExecutionReceiptRequest, ArtifactPromotionExecutionReceiptV1,
7 ArtifactPromotionPlanV1, ArtifactPromotionProvenanceReportRequest, ArtifactTransportV1,
8 CurrentCliDeploymentExecutor, DeploymentCheckV1, DeploymentCommandResultV1,
9 DeploymentExecutionContextV1, DeploymentExecutionPreflightV1, DeploymentExecutionStatusV1,
10 DeploymentExecutor, DeploymentExecutorCapabilityV1, DeploymentPlanV1, DeploymentReceiptV1,
11 LocalDeploymentCheckRequest, LocalInventoryRequest, ObservationStatusV1, SafetyFindingV1,
12 StagingReceiptV1, artifact_gate_phase_receipt, artifact_gate_role_phase_receipts,
13 artifact_promotion_execution_receipt, artifact_promotion_provenance_report,
14 check_local_deployment, collect_local_deployment_inventory, compare_plan_to_inventory,
15 deployment_execution_preflight_from_check, deployment_receipt_from_check_with_status,
16 missing_executor_capabilities, phase_receipt, safety_report_from_diff,
17 staging_receipt_evidence, validate_deployment_execution_preflight_for_check,
18};
19use crate::format::wasm_size_label;
20use crate::icp::{self, CANIC_ICP_LOCAL_NETWORK_URL_ENV, CANIC_ICP_LOCAL_ROOT_KEY_ENV};
21use crate::release_set::{
22 LOCAL_ROOT_MIN_READY_CYCLES, RootReleaseSetManifest, configured_fleet_name,
23 configured_install_targets, configured_local_root_create_cycles,
24 emit_root_release_set_manifest_with_config, icp_query_on_network, icp_root,
25 load_root_release_set_manifest, resolve_artifact_root, resume_root_bootstrap,
26 stage_root_release_set, workspace_root,
27};
28use crate::replica_query;
29use crate::response_parse::parse_cycle_balance_response;
30use crate::table::{ColumnAlign, render_separator, render_table, render_table_row, table_widths};
31use canic_core::cdk::utils::hash::wasm_hash_hex;
32use canic_core::{
33 CANIC_WASM_CHUNK_BYTES,
34 cdk::{types::Principal, utils::hash::wasm_hash},
35 protocol,
36};
37use config_selection::resolve_install_config_path;
38use serde_json::Value as JsonValue;
39use std::{
40 env,
41 ffi::OsString,
42 fs,
43 path::{Path, PathBuf},
44 process::Command,
45 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
46};
47
48mod config_selection;
49mod readiness;
50mod state;
51
52pub use config_selection::{
53 current_canic_project_root, discover_canic_config_choices, discover_canic_project_root_from,
54 discover_project_canic_config_choices, project_fleet_roots,
55};
56use readiness::wait_for_root_ready;
57use state::{
58 INSTALL_STATE_SCHEMA_VERSION, validate_fleet_name, validate_network_name, write_install_state,
59};
60pub use state::{
61 InstallState, read_named_fleet_install_state, read_named_fleet_install_state_from_root,
62};
63
64#[cfg(test)]
65mod tests;
66
67#[cfg(test)]
68use config_selection::config_selection_error;
69#[cfg(test)]
70use readiness::{parse_bootstrap_status_value, parse_root_ready_value};
71#[cfg(test)]
72use state::{fleet_install_state_path, read_fleet_install_state};
73
74#[derive(Clone, Debug)]
79pub struct InstallRootOptions {
80 pub root_canister: String,
81 pub root_build_target: String,
82 pub network: String,
83 pub icp_root: Option<PathBuf>,
84 pub build_profile: Option<CanisterBuildProfile>,
85 pub ready_timeout_seconds: u64,
86 pub config_path: Option<String>,
87 pub expected_fleet: Option<String>,
88 pub interactive_config_selection: bool,
89 pub deployment_plan_override: Option<DeploymentPlanV1>,
90 pub artifact_promotion_plan_override: Option<ArtifactPromotionPlanV1>,
91}
92
93#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
98struct InstallTimingSummary {
99 create_canisters: Duration,
100 build_all: Duration,
101 emit_manifest: Duration,
102 install_root: Duration,
103 fund_root: Duration,
104 stage_release_set: Duration,
105 resume_bootstrap: Duration,
106 wait_ready: Duration,
107 finalize_root_funding: Duration,
108}
109
110const CURRENT_INSTALL_REQUIRED_CAPABILITIES: &[DeploymentExecutorCapabilityV1] = &[
111 DeploymentExecutorCapabilityV1::CreateCanister,
112 DeploymentExecutorCapabilityV1::InstallCode,
113 DeploymentExecutorCapabilityV1::Call,
114 DeploymentExecutorCapabilityV1::Query,
115 DeploymentExecutorCapabilityV1::StageArtifact,
116];
117
118pub fn discover_current_canic_config_choices() -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
120 let project_root = current_canic_project_root()?;
121 let choices = config_selection::discover_workspace_canic_config_choices(&project_root)?;
122 if !choices.is_empty() {
123 return Ok(choices);
124 }
125
126 if let Ok(icp_root) = icp_root()
127 && icp_root != project_root
128 {
129 return config_selection::discover_workspace_canic_config_choices(&icp_root);
130 }
131
132 Ok(choices)
133}
134
135pub fn install_root(options: InstallRootOptions) -> Result<(), Box<dyn std::error::Error>> {
137 let workspace_root = workspace_root()?;
138 let icp_root = match &options.icp_root {
139 Some(path) => path.canonicalize()?,
140 None => icp_root()?,
141 };
142 let config_path = resolve_install_config_path(
143 &icp_root,
144 options.config_path.as_deref(),
145 options.interactive_config_selection,
146 )?;
147 let _install_env = BuildEnvGuard::apply(&options.network, &config_path, &icp_root);
148 let fleet_name = configured_fleet_name(&config_path)?;
149 validate_expected_fleet_name(options.expected_fleet.as_deref(), &fleet_name, &config_path)?;
150 validate_fleet_name(&fleet_name)?;
151 let total_started_at = Instant::now();
152 let mut timings = InstallTimingSummary::default();
153 let network = options.network.as_str();
154 let execution_context = current_install_execution_context(&workspace_root, &icp_root, network);
155
156 println!("Installing fleet {fleet_name}");
157 println!();
158 let prepared = prepare_install_deployment_truth(
159 &options,
160 &workspace_root,
161 &icp_root,
162 &config_path,
163 &fleet_name,
164 &execution_context,
165 )?;
166 timings.create_canisters = prepared.timings.create_canisters;
167 timings.build_all = prepared.timings.build_all;
168
169 let (manifest_path, emit_manifest_duration) = emit_manifest_with_deployment_truth_receipt(
170 &workspace_root,
171 &icp_root,
172 &options,
173 &config_path,
174 &fleet_name,
175 &prepared.deployment_truth_check,
176 &execution_context,
177 )?;
178 timings.emit_manifest = emit_manifest_duration;
179 let activation_timings = run_root_activation_phases(
180 InstallReceiptScope {
181 icp_root: &icp_root,
182 network,
183 fleet_name: &fleet_name,
184 check: &prepared.deployment_truth_check,
185 execution_context: Some(&execution_context),
186 },
187 &options,
188 &prepared.root_canister_id,
189 &manifest_path,
190 total_started_at,
191 )?;
192 timings.install_root = activation_timings.install_root;
193 timings.fund_root = activation_timings.fund_root;
194 timings.stage_release_set = activation_timings.stage_release_set;
195 timings.resume_bootstrap = activation_timings.resume_bootstrap;
196 timings.wait_ready = activation_timings.wait_ready;
197 timings.finalize_root_funding = activation_timings.finalize_root_funding;
198
199 print_install_timing_summary(&timings, total_started_at.elapsed());
200 let state = build_install_state(
201 &options,
202 &workspace_root,
203 &icp_root,
204 &config_path,
205 &manifest_path,
206 &fleet_name,
207 &prepared.root_canister_id,
208 )?;
209 let state_path = write_install_state_with_deployment_truth_receipt(
210 InstallReceiptScope {
211 icp_root: &icp_root,
212 network,
213 fleet_name: &fleet_name,
214 check: &prepared.deployment_truth_check,
215 execution_context: Some(&execution_context),
216 },
217 &options.network,
218 &state,
219 )?;
220 write_artifact_promotion_execution_receipt_for_install(
221 &options,
222 &icp_root,
223 network,
224 &fleet_name,
225 &prepared.deployment_truth_check,
226 &execution_context,
227 )?;
228 print_install_result_summary(&options.network, &state.fleet, &state_path);
229 Ok(())
230}
231
232struct PreparedInstallTruth {
233 root_canister_id: String,
234 deployment_truth_check: DeploymentCheckV1,
235 timings: InstallTimingSummary,
236}
237
238struct CurrentInstallTruthInputs {
239 workspace_root: PathBuf,
240 icp_root: PathBuf,
241 config_path: PathBuf,
242 fleet_name: String,
243}
244
245fn prepare_install_deployment_truth(
246 options: &InstallRootOptions,
247 workspace_root: &Path,
248 icp_root: &Path,
249 config_path: &Path,
250 fleet_name: &str,
251 execution_context: &DeploymentExecutionContextV1,
252) -> Result<PreparedInstallTruth, Box<dyn std::error::Error>> {
253 let mut timings = InstallTimingSummary::default();
254 ensure_current_install_executor_capabilities(execution_context)?;
255 ensure_icp_environment_ready(icp_root, &options.network)?;
256 let (root_canister_id, create_phase, create_duration) =
257 resolve_root_canister_with_phase(options, icp_root, config_path)?;
258 timings.create_canisters = create_duration;
259
260 let (build_phase, build_duration) =
261 build_install_targets_with_phase(options, icp_root, config_path)?;
262 timings.build_all = build_duration;
263
264 let deployment_truth_check = run_install_deployment_truth_safety_gate(
265 options,
266 workspace_root,
267 icp_root,
268 config_path,
269 fleet_name,
270 execution_context,
271 )?;
272 let receipt_scope = InstallReceiptScope {
273 icp_root,
274 network: &options.network,
275 fleet_name,
276 check: &deployment_truth_check,
277 execution_context: Some(execution_context),
278 };
279 write_completed_install_phase_receipt(receipt_scope, create_phase)?;
280 write_completed_install_phase_receipt(receipt_scope, build_phase)?;
281
282 Ok(PreparedInstallTruth {
283 root_canister_id,
284 deployment_truth_check,
285 timings,
286 })
287}
288
289fn resolve_root_canister_with_phase(
290 options: &InstallRootOptions,
291 icp_root: &Path,
292 config_path: &Path,
293) -> Result<(String, CompletedInstallPhase, Duration), Box<dyn std::error::Error>> {
294 let operation = ResolveRootCanisterOperation::new(
295 icp_root,
296 &options.network,
297 &options.root_canister,
298 config_path,
299 );
300 let started_at = current_unix_timestamp_label()?;
301 let started = Instant::now();
302 let root_canister_id = operation.execute()?;
303 let duration = started.elapsed();
304 let phase = CompletedInstallPhase {
305 phase: "resolve_root_canister",
306 attempted_action: "resolve or create root canister id",
307 started_at,
308 finished_at: Some(current_unix_timestamp_label()?),
309 evidence: operation.evidence(&root_canister_id),
310 role_names: Vec::new(),
311 };
312 Ok((root_canister_id, phase, duration))
313}
314
315fn build_install_targets_with_phase(
316 options: &InstallRootOptions,
317 icp_root: &Path,
318 config_path: &Path,
319) -> Result<(CompletedInstallPhase, Duration), Box<dyn std::error::Error>> {
320 if let Some(plan) = &options.deployment_plan_override {
321 return validate_plan_artifacts_with_phase(plan, icp_root, &options.network);
322 }
323
324 let build_targets = configured_install_targets(config_path, &options.root_build_target)?;
325 let operation = BuildInstallTargetsOperation::new(
326 &options.network,
327 build_targets,
328 options.build_profile,
329 config_path,
330 icp_root,
331 );
332 let started_at = current_unix_timestamp_label()?;
333 let started = Instant::now();
334 operation.execute()?;
335 let duration = started.elapsed();
336 let phase = CompletedInstallPhase {
337 phase: "build_artifacts",
338 attempted_action: "build configured install targets",
339 started_at,
340 finished_at: Some(current_unix_timestamp_label()?),
341 evidence: operation.evidence(),
342 role_names: operation.role_names(),
343 };
344 Ok((phase, duration))
345}
346
347fn validate_plan_artifacts_with_phase(
348 plan: &DeploymentPlanV1,
349 icp_root: &Path,
350 network: &str,
351) -> Result<(CompletedInstallPhase, Duration), Box<dyn std::error::Error>> {
352 let started_at = current_unix_timestamp_label()?;
353 let started = Instant::now();
354 validate_plan_artifact_paths(plan, icp_root, network)?;
355 let duration = started.elapsed();
356 let role_names = plan
357 .role_artifacts
358 .iter()
359 .map(|artifact| artifact.role.clone())
360 .collect::<Vec<_>>();
361 let phase = CompletedInstallPhase {
362 phase: "materialize_artifacts",
363 attempted_action: "validate supplied deployment plan artifacts",
364 started_at,
365 finished_at: Some(current_unix_timestamp_label()?),
366 evidence: vec![format!("deployment_plan:{}", plan.plan_id)],
367 role_names,
368 };
369 Ok((phase, duration))
370}
371
372fn validate_plan_artifact_paths(
373 plan: &DeploymentPlanV1,
374 icp_root: &Path,
375 network: &str,
376) -> Result<(), Box<dyn std::error::Error>> {
377 let artifact_root = resolve_artifact_root(icp_root, network)?;
378 let root_artifact = plan
379 .role_artifacts
380 .iter()
381 .find(|artifact| artifact.role == "root")
382 .ok_or_else(|| "deployment plan is missing root role artifact".to_string())?;
383 let root_wasm = plan_role_wasm_path(icp_root, &artifact_root, root_artifact);
384 if !root_wasm.is_file() {
385 return Err(format!(
386 "deployment plan root wasm artifact does not exist: {}",
387 root_wasm.display()
388 )
389 .into());
390 }
391
392 for artifact in plan_release_role_artifacts(plan) {
393 let wasm_gz = plan_role_wasm_gz_path(icp_root, &artifact_root, artifact);
394 if !wasm_gz.is_file() {
395 return Err(format!(
396 "deployment plan role {} wasm.gz artifact does not exist: {}",
397 artifact.role,
398 wasm_gz.display()
399 )
400 .into());
401 }
402 }
403 Ok(())
404}
405
406fn emit_manifest_with_deployment_truth_receipt(
407 workspace_root: &Path,
408 icp_root: &Path,
409 options: &InstallRootOptions,
410 config_path: &Path,
411 fleet_name: &str,
412 deployment_truth_check: &DeploymentCheckV1,
413 execution_context: &DeploymentExecutionContextV1,
414) -> Result<(PathBuf, Duration), Box<dyn std::error::Error>> {
415 let operation =
416 EmitRootManifestOperation::new(workspace_root, icp_root, &options.network, config_path);
417 let emit_manifest_started_at_label = current_unix_timestamp_label()?;
418 let emit_manifest_started_at = Instant::now();
419 let manifest_path = if let Some(plan) = &options.deployment_plan_override {
420 emit_root_release_set_manifest_from_plan(icp_root, &options.network, plan)?
421 } else {
422 operation.execute()?
423 };
424 let emit_manifest_duration = emit_manifest_started_at.elapsed();
425 let emit_manifest_receipt = receipt_with_execution_context(
426 install_deployment_truth_phase_receipt(
427 deployment_truth_check,
428 "emit_manifest",
429 emit_manifest_started_at_label,
430 Some(current_unix_timestamp_label()?),
431 "emit root release-set manifest",
432 crate::deployment_truth::ObservationStatusV1::Observed,
433 EmitRootManifestOperation::evidence(&manifest_path),
434 ),
435 execution_context,
436 );
437 let emit_manifest_receipt_path = write_install_deployment_truth_receipt(
438 icp_root,
439 &options.network,
440 fleet_name,
441 &emit_manifest_receipt,
442 )?;
443 println!(
444 "Deployment truth receipt JSON: {}",
445 emit_manifest_receipt_path.display()
446 );
447 Ok((manifest_path, emit_manifest_duration))
448}
449
450fn emit_root_release_set_manifest_from_plan(
451 icp_root: &Path,
452 network: &str,
453 plan: &DeploymentPlanV1,
454) -> Result<PathBuf, Box<dyn std::error::Error>> {
455 let artifact_root = resolve_artifact_root(icp_root, network)?;
456 let manifest_path = crate::release_set::root_release_set_manifest_path(&artifact_root)?;
457 let entries = plan_release_role_artifacts(plan)
458 .map(|artifact| release_set_entry_from_plan_artifact(icp_root, &artifact_root, artifact))
459 .collect::<Result<Vec<_>, _>>()?;
460 let manifest = RootReleaseSetManifest {
461 release_version: plan
462 .deployment_identity
463 .canic_version
464 .clone()
465 .unwrap_or_else(|| plan.plan_id.clone()),
466 entries,
467 };
468
469 fs::write(&manifest_path, serde_json::to_vec_pretty(&manifest)?)?;
470 Ok(manifest_path)
471}
472
473fn release_set_entry_from_plan_artifact(
474 icp_root: &Path,
475 artifact_root: &Path,
476 artifact: &crate::deployment_truth::RoleArtifactV1,
477) -> Result<crate::release_set::ReleaseSetEntry, Box<dyn std::error::Error>> {
478 let artifact_path = plan_role_wasm_gz_path(icp_root, artifact_root, artifact);
479 let artifact_relative_path = artifact_path
480 .strip_prefix(icp_root)
481 .map_err(|_| {
482 format!(
483 "deployment plan artifact {} is not under ICP root {}",
484 artifact_path.display(),
485 icp_root.display()
486 )
487 })?
488 .to_string_lossy()
489 .to_string();
490 let wasm_module = fs::read(&artifact_path)?;
491 let chunk_hashes = wasm_module
492 .chunks(CANIC_WASM_CHUNK_BYTES)
493 .map(wasm_hash_hex)
494 .collect::<Vec<_>>();
495
496 Ok(crate::release_set::ReleaseSetEntry {
497 role: artifact.role.clone(),
498 template_id: format!("embedded:{}", artifact.role),
499 artifact_relative_path,
500 payload_size_bytes: wasm_module.len() as u64,
501 payload_sha256_hex: wasm_hash_hex(&wasm_module),
502 chunk_size_bytes: CANIC_WASM_CHUNK_BYTES as u64,
503 chunk_sha256_hex: chunk_hashes,
504 })
505}
506
507fn plan_release_role_artifacts(
508 plan: &DeploymentPlanV1,
509) -> impl Iterator<Item = &crate::deployment_truth::RoleArtifactV1> {
510 plan.role_artifacts
511 .iter()
512 .filter(|artifact| !matches!(artifact.role.as_str(), "root" | "wasm_store"))
513}
514
515fn plan_role_wasm_path(
516 icp_root: &Path,
517 artifact_root: &Path,
518 artifact: &crate::deployment_truth::RoleArtifactV1,
519) -> PathBuf {
520 artifact.wasm_path.as_ref().map_or_else(
521 || {
522 artifact_root
523 .join(&artifact.role)
524 .join(format!("{}.wasm", artifact.role))
525 },
526 |path| plan_artifact_path(icp_root, path),
527 )
528}
529
530fn plan_role_wasm_gz_path(
531 icp_root: &Path,
532 artifact_root: &Path,
533 artifact: &crate::deployment_truth::RoleArtifactV1,
534) -> PathBuf {
535 artifact.wasm_gz_path.as_ref().map_or_else(
536 || {
537 artifact_root
538 .join(&artifact.role)
539 .join(format!("{}.wasm.gz", artifact.role))
540 },
541 |path| plan_artifact_path(icp_root, path),
542 )
543}
544
545fn plan_artifact_path(icp_root: &Path, path: &str) -> PathBuf {
546 let path = PathBuf::from(path);
547 if path.is_absolute() {
548 path
549 } else {
550 icp_root.join(path)
551 }
552}
553
554fn run_root_activation_phases(
555 receipt_scope: InstallReceiptScope<'_>,
556 options: &InstallRootOptions,
557 root_canister_id: &str,
558 manifest_path: &Path,
559 total_started_at: Instant,
560) -> Result<InstallTimingSummary, Box<dyn std::error::Error>> {
561 let mut timings = InstallTimingSummary::default();
562 let root_wasm = root_wasm_for_install_plan(
563 receipt_scope.icp_root,
564 receipt_scope.network,
565 &options.root_build_target,
566 options.deployment_plan_override.as_ref(),
567 )?;
568 let install_operation = InstallRootWasmOperation::new(
569 receipt_scope.icp_root,
570 receipt_scope.network,
571 root_canister_id,
572 root_wasm,
573 );
574 timings.install_root = receipt_scope.run_operation(&install_operation)?;
575 let pre_bootstrap_funding = EnsureRootCyclesOperation::new(
576 receipt_scope.icp_root,
577 receipt_scope.network,
578 root_canister_id,
579 "fund_root_pre_bootstrap",
580 "ensure local root minimum cycles before bootstrap",
581 "pre-bootstrap",
582 );
583 timings.fund_root = receipt_scope.run_operation(&pre_bootstrap_funding)?;
584 let manifest = load_root_release_set_manifest(manifest_path)?;
585 let stage_operation = StageReleaseSetOperation::new(
586 receipt_scope.icp_root,
587 receipt_scope.network,
588 root_canister_id,
589 manifest_path,
590 manifest,
591 );
592 timings.stage_release_set = receipt_scope.run_operation(&stage_operation)?;
593 let resume_operation = ResumeBootstrapOperation::new(receipt_scope.network, root_canister_id);
594 timings.resume_bootstrap = receipt_scope.run_operation(&resume_operation)?;
595 let wait_ready_operation = WaitRootReadyOperation::new(
596 receipt_scope.network,
597 root_canister_id,
598 options.ready_timeout_seconds,
599 );
600 let wait_ready_result = receipt_scope.run_operation(&wait_ready_operation);
601 match wait_ready_result {
602 Ok(duration) => timings.wait_ready = duration,
603 Err(err) => {
604 print_install_timing_summary(&timings, total_started_at.elapsed());
605 return Err(err);
606 }
607 }
608 let post_ready_funding = EnsureRootCyclesOperation::new(
609 receipt_scope.icp_root,
610 receipt_scope.network,
611 root_canister_id,
612 "fund_root_post_ready",
613 "ensure local root minimum cycles after ready",
614 "post-ready",
615 );
616 timings.finalize_root_funding = receipt_scope.run_operation(&post_ready_funding)?;
617 Ok(timings)
618}
619
620fn root_wasm_for_install_plan(
621 icp_root: &Path,
622 network: &str,
623 root_build_target: &str,
624 plan: Option<&DeploymentPlanV1>,
625) -> Result<PathBuf, Box<dyn std::error::Error>> {
626 let artifact_root = resolve_artifact_root(icp_root, network)?;
627 if let Some(plan) = plan {
628 let root_artifact = plan
629 .role_artifacts
630 .iter()
631 .find(|artifact| artifact.role == "root")
632 .ok_or_else(|| "deployment plan is missing root role artifact".to_string())?;
633 return Ok(plan_role_wasm_path(icp_root, &artifact_root, root_artifact));
634 }
635
636 Ok(artifact_root
637 .join(root_build_target)
638 .join(format!("{root_build_target}.wasm")))
639}
640
641#[derive(Clone, Copy)]
642struct InstallReceiptScope<'a> {
643 icp_root: &'a Path,
644 network: &'a str,
645 fleet_name: &'a str,
646 check: &'a DeploymentCheckV1,
647 execution_context: Option<&'a DeploymentExecutionContextV1>,
648}
649
650struct CompletedInstallPhase {
651 phase: &'static str,
652 attempted_action: &'static str,
653 started_at: String,
654 finished_at: Option<String>,
655 evidence: Vec<String>,
656 role_names: Vec<String>,
657}
658
659trait InstallPhaseOperation {
660 fn phase(&self) -> &'static str;
661 fn attempted_action(&self) -> &'static str;
662 fn evidence(&self) -> Vec<String>;
663 fn execute(&self) -> Result<(), Box<dyn std::error::Error>>;
664}
665
666struct ResolveRootCanisterOperation<'a> {
667 icp_root: &'a Path,
668 network: &'a str,
669 root_canister: &'a str,
670 config_path: &'a Path,
671}
672
673impl<'a> ResolveRootCanisterOperation<'a> {
674 const fn new(
675 icp_root: &'a Path,
676 network: &'a str,
677 root_canister: &'a str,
678 config_path: &'a Path,
679 ) -> Self {
680 Self {
681 icp_root,
682 network,
683 root_canister,
684 config_path,
685 }
686 }
687
688 fn evidence(&self, root_canister_id: &str) -> Vec<String> {
689 vec![
690 format!("root_target:{}", self.root_canister),
691 format!("root_canister:{root_canister_id}"),
692 ]
693 }
694
695 fn execute(&self) -> Result<String, Box<dyn std::error::Error>> {
696 ensure_root_canister_id(
697 self.icp_root,
698 self.network,
699 self.root_canister,
700 self.config_path,
701 )
702 }
703}
704
705struct BuildInstallTargetsOperation<'a> {
706 network: &'a str,
707 build_targets: Vec<String>,
708 build_profile: Option<CanisterBuildProfile>,
709 config_path: &'a Path,
710 icp_root: &'a Path,
711}
712
713impl<'a> BuildInstallTargetsOperation<'a> {
714 const fn new(
715 network: &'a str,
716 build_targets: Vec<String>,
717 build_profile: Option<CanisterBuildProfile>,
718 config_path: &'a Path,
719 icp_root: &'a Path,
720 ) -> Self {
721 Self {
722 network,
723 build_targets,
724 build_profile,
725 config_path,
726 icp_root,
727 }
728 }
729
730 fn evidence(&self) -> Vec<String> {
731 self.build_targets
732 .iter()
733 .map(|target| format!("build_target:{target}"))
734 .collect()
735 }
736
737 fn role_names(&self) -> Vec<String> {
738 self.build_targets.clone()
739 }
740
741 fn execute(&self) -> Result<(), Box<dyn std::error::Error>> {
742 run_canic_build_targets(
743 self.network,
744 &self.build_targets,
745 self.build_profile,
746 self.config_path,
747 self.icp_root,
748 )
749 }
750}
751
752struct EmitRootManifestOperation<'a> {
753 workspace_root: &'a Path,
754 icp_root: &'a Path,
755 network: &'a str,
756 config_path: &'a Path,
757}
758
759impl<'a> EmitRootManifestOperation<'a> {
760 const fn new(
761 workspace_root: &'a Path,
762 icp_root: &'a Path,
763 network: &'a str,
764 config_path: &'a Path,
765 ) -> Self {
766 Self {
767 workspace_root,
768 icp_root,
769 network,
770 config_path,
771 }
772 }
773
774 fn evidence(manifest_path: &Path) -> Vec<String> {
775 vec![format!("manifest_path:{}", manifest_path.display())]
776 }
777
778 fn execute(&self) -> Result<PathBuf, Box<dyn std::error::Error>> {
779 emit_root_release_set_manifest_with_config(
780 self.workspace_root,
781 self.icp_root,
782 self.network,
783 self.config_path,
784 )
785 }
786}
787
788struct InstallRootWasmOperation<'a> {
789 icp_root: &'a Path,
790 network: &'a str,
791 root_canister_id: &'a str,
792 root_wasm: PathBuf,
793}
794
795impl<'a> InstallRootWasmOperation<'a> {
796 const fn new(
797 icp_root: &'a Path,
798 network: &'a str,
799 root_canister_id: &'a str,
800 root_wasm: PathBuf,
801 ) -> Self {
802 Self {
803 icp_root,
804 network,
805 root_canister_id,
806 root_wasm,
807 }
808 }
809}
810
811impl InstallPhaseOperation for InstallRootWasmOperation<'_> {
812 fn phase(&self) -> &'static str {
813 "install_root"
814 }
815
816 fn attempted_action(&self) -> &'static str {
817 "install root wasm"
818 }
819
820 fn evidence(&self) -> Vec<String> {
821 vec![
822 format!("root_canister:{}", self.root_canister_id),
823 format!("root_wasm:{}", self.root_wasm.display()),
824 ]
825 }
826
827 fn execute(&self) -> Result<(), Box<dyn std::error::Error>> {
828 reinstall_root_wasm(
829 self.icp_root,
830 self.network,
831 self.root_canister_id,
832 &self.root_wasm,
833 )
834 }
835}
836
837struct EnsureRootCyclesOperation<'a> {
838 icp_root: &'a Path,
839 network: &'a str,
840 root_canister_id: &'a str,
841 phase: &'static str,
842 attempted_action: &'static str,
843 phase_label: &'a str,
844}
845
846impl<'a> EnsureRootCyclesOperation<'a> {
847 const fn new(
848 icp_root: &'a Path,
849 network: &'a str,
850 root_canister_id: &'a str,
851 phase: &'static str,
852 attempted_action: &'static str,
853 phase_label: &'a str,
854 ) -> Self {
855 Self {
856 icp_root,
857 network,
858 root_canister_id,
859 phase,
860 attempted_action,
861 phase_label,
862 }
863 }
864}
865
866impl InstallPhaseOperation for EnsureRootCyclesOperation<'_> {
867 fn phase(&self) -> &'static str {
868 self.phase
869 }
870
871 fn attempted_action(&self) -> &'static str {
872 self.attempted_action
873 }
874
875 fn evidence(&self) -> Vec<String> {
876 vec![
877 format!("root_canister:{}", self.root_canister_id),
878 format!("minimum_cycles:{LOCAL_ROOT_MIN_READY_CYCLES}"),
879 format!("funding_phase:{}", self.phase_label),
880 ]
881 }
882
883 fn execute(&self) -> Result<(), Box<dyn std::error::Error>> {
884 ensure_local_root_min_cycles(
885 self.icp_root,
886 self.network,
887 self.root_canister_id,
888 self.phase_label,
889 )
890 }
891}
892
893struct ResumeBootstrapOperation<'a> {
894 network: &'a str,
895 root_canister_id: &'a str,
896}
897
898impl<'a> ResumeBootstrapOperation<'a> {
899 const fn new(network: &'a str, root_canister_id: &'a str) -> Self {
900 Self {
901 network,
902 root_canister_id,
903 }
904 }
905}
906
907impl InstallPhaseOperation for ResumeBootstrapOperation<'_> {
908 fn phase(&self) -> &'static str {
909 "resume_bootstrap"
910 }
911
912 fn attempted_action(&self) -> &'static str {
913 "resume root bootstrap"
914 }
915
916 fn evidence(&self) -> Vec<String> {
917 vec![format!("root_canister:{}", self.root_canister_id)]
918 }
919
920 fn execute(&self) -> Result<(), Box<dyn std::error::Error>> {
921 resume_root_bootstrap(self.network, self.root_canister_id)
922 }
923}
924
925struct WaitRootReadyOperation<'a> {
926 network: &'a str,
927 root_canister_id: &'a str,
928 timeout_seconds: u64,
929}
930
931impl<'a> WaitRootReadyOperation<'a> {
932 const fn new(network: &'a str, root_canister_id: &'a str, timeout_seconds: u64) -> Self {
933 Self {
934 network,
935 root_canister_id,
936 timeout_seconds,
937 }
938 }
939}
940
941impl InstallPhaseOperation for WaitRootReadyOperation<'_> {
942 fn phase(&self) -> &'static str {
943 "wait_ready"
944 }
945
946 fn attempted_action(&self) -> &'static str {
947 "wait for root bootstrap readiness"
948 }
949
950 fn evidence(&self) -> Vec<String> {
951 vec![
952 format!("root_canister:{}", self.root_canister_id),
953 format!("timeout_seconds:{}", self.timeout_seconds),
954 ]
955 }
956
957 fn execute(&self) -> Result<(), Box<dyn std::error::Error>> {
958 wait_for_root_ready(self.network, self.root_canister_id, self.timeout_seconds)
959 }
960}
961
962fn write_completed_install_phase_receipt(
963 receipt_scope: InstallReceiptScope<'_>,
964 completed: CompletedInstallPhase,
965) -> Result<PathBuf, Box<dyn std::error::Error>> {
966 let role_phase_receipts = completed
967 .role_names
968 .iter()
969 .filter_map(|role| {
970 completed_phase_role_receipt(
971 receipt_scope.check,
972 completed.phase,
973 role,
974 crate::deployment_truth::RolePhaseResultV1::Applied,
975 None,
976 )
977 })
978 .collect();
979 let receipt =
980 receipt_scope.with_execution_context(install_deployment_truth_phase_receipt_with_result(
981 receipt_scope.check,
982 PhaseReceiptInput {
983 phase: completed.phase,
984 started_at: completed.started_at,
985 finished_at: completed.finished_at,
986 attempted_action: completed.attempted_action,
987 status: crate::deployment_truth::ObservationStatusV1::Observed,
988 evidence: completed.evidence,
989 role_phase_receipts,
990 operation_status: DeploymentExecutionStatusV1::Complete,
991 command_result: DeploymentCommandResultV1::Succeeded,
992 },
993 ));
994 receipt_scope.write_receipt(&receipt)
995}
996
997fn completed_phase_role_receipt(
998 check: &DeploymentCheckV1,
999 phase: &str,
1000 role: &str,
1001 result: crate::deployment_truth::RolePhaseResultV1,
1002 error: Option<String>,
1003) -> Option<crate::deployment_truth::RolePhaseReceiptV1> {
1004 let planned = check
1005 .plan
1006 .role_artifacts
1007 .iter()
1008 .find(|artifact| artifact.role == role)?;
1009 let observed = check
1010 .inventory
1011 .observed_artifacts
1012 .iter()
1013 .find(|artifact| artifact.role == role);
1014 let artifact_digest = observed
1015 .and_then(|artifact| artifact.file_sha256.clone())
1016 .or_else(|| observed.and_then(|artifact| artifact.payload_sha256.clone()))
1017 .or_else(|| planned.observed_wasm_gz_file_sha256.clone())
1018 .or_else(|| planned.wasm_gz_sha256.clone());
1019
1020 Some(crate::deployment_truth::RolePhaseReceiptV1 {
1021 role: role.to_string(),
1022 phase: phase.to_string(),
1023 result,
1024 previous_module_hash: None,
1025 target_module_hash: planned.installed_module_hash.clone(),
1026 observed_module_hash_after: None,
1027 artifact_digest,
1028 canonical_embedded_config_sha256: planned.canonical_embedded_config_sha256.clone(),
1029 error,
1030 })
1031}
1032
1033fn write_install_state_with_deployment_truth_receipt(
1034 receipt_scope: InstallReceiptScope<'_>,
1035 network: &str,
1036 state: &InstallState,
1037) -> Result<PathBuf, Box<dyn std::error::Error>> {
1038 let started_at = current_unix_timestamp_label()?;
1039 let state_path = write_install_state(receipt_scope.icp_root, network, state)?;
1040 let completed = CompletedInstallPhase {
1041 phase: "write_install_state",
1042 attempted_action: "write local install state",
1043 started_at,
1044 finished_at: Some(current_unix_timestamp_label()?),
1045 evidence: vec![
1046 format!("install_state:{}", state_path.display()),
1047 format!("fleet:{}", state.fleet),
1048 format!("root_canister:{}", state.root_canister_id),
1049 ],
1050 role_names: Vec::new(),
1051 };
1052 write_completed_install_phase_receipt(receipt_scope, completed)?;
1053 Ok(state_path)
1054}
1055
1056fn write_artifact_promotion_execution_receipt_for_install(
1057 options: &InstallRootOptions,
1058 icp_root: &Path,
1059 network: &str,
1060 fleet_name: &str,
1061 check: &DeploymentCheckV1,
1062 execution_context: &DeploymentExecutionContextV1,
1063) -> Result<Option<PathBuf>, Box<dyn std::error::Error>> {
1064 let Some(promotion_plan) = &options.artifact_promotion_plan_override else {
1065 return Ok(None);
1066 };
1067 let deployment_receipt =
1068 promotion_install_deployment_receipt(check, execution_context, promotion_plan)?;
1069 let provenance_report =
1070 artifact_promotion_provenance_report(ArtifactPromotionProvenanceReportRequest {
1071 report_id: format!("{}:execution-provenance", promotion_plan.plan_id),
1072 artifact_promotion_plan: promotion_plan.clone(),
1073 wasm_store_identity_report: None,
1074 wasm_store_catalog_verification: None,
1075 materialization_identity_report: None,
1076 })?;
1077 let receipt = artifact_promotion_execution_receipt(ArtifactPromotionExecutionReceiptRequest {
1078 receipt_id: format!("{}:execution-receipt", promotion_plan.plan_id),
1079 provenance_report,
1080 deployment_receipt,
1081 })?;
1082 let path = write_artifact_promotion_execution_receipt(icp_root, network, fleet_name, &receipt)?;
1083 println!(
1084 "Artifact promotion execution receipt JSON: {}",
1085 path.display()
1086 );
1087 Ok(Some(path))
1088}
1089
1090fn promotion_install_deployment_receipt(
1091 check: &DeploymentCheckV1,
1092 execution_context: &DeploymentExecutionContextV1,
1093 promotion_plan: &ArtifactPromotionPlanV1,
1094) -> Result<DeploymentReceiptV1, Box<dyn std::error::Error>> {
1095 let started_at = current_unix_timestamp_label()?;
1096 let finished_at = current_unix_timestamp_label()?;
1097 let phase = phase_receipt(
1098 "promoted_plan_install",
1099 started_at.clone(),
1100 Some(finished_at.clone()),
1101 "execute promoted deployment plan through current install runner",
1102 ObservationStatusV1::Observed,
1103 vec![
1104 format!("artifact_promotion_plan:{}", promotion_plan.plan_id),
1105 format!(
1106 "artifact_promotion_plan_digest:{}",
1107 promotion_plan.artifact_promotion_plan_digest
1108 ),
1109 format!(
1110 "promotion_plan_lineage_digest:{}",
1111 promotion_plan.promotion_plan_lineage_digest
1112 ),
1113 ],
1114 );
1115 let role_phase_receipts = promotion_plan
1116 .transform
1117 .roles
1118 .iter()
1119 .map(|role| {
1120 completed_phase_role_receipt(
1121 check,
1122 "promoted_plan_install",
1123 &role.role,
1124 crate::deployment_truth::RolePhaseResultV1::Applied,
1125 None,
1126 )
1127 .ok_or_else(|| {
1128 format!(
1129 "promoted role {} is missing from deployment plan artifacts",
1130 role.role
1131 )
1132 })
1133 })
1134 .collect::<Result<Vec<_>, _>>()?;
1135 Ok(receipt_with_execution_context(
1136 deployment_receipt_from_check_with_status(
1137 check,
1138 format!("{}:promoted_plan_install", check.check_id),
1139 DeploymentExecutionStatusV1::Complete,
1140 started_at,
1141 Some(finished_at),
1142 vec![phase],
1143 role_phase_receipts,
1144 DeploymentCommandResultV1::Succeeded,
1145 ),
1146 execution_context,
1147 ))
1148}
1149
1150impl InstallReceiptScope<'_> {
1151 fn run_operation(
1152 self,
1153 operation: &impl InstallPhaseOperation,
1154 ) -> Result<Duration, Box<dyn std::error::Error>> {
1155 self.run_phase(
1156 operation.phase(),
1157 operation.attempted_action(),
1158 operation.evidence(),
1159 || operation.execute(),
1160 )
1161 }
1162
1163 fn run_phase(
1164 self,
1165 phase: &str,
1166 attempted_action: &str,
1167 evidence: Vec<String>,
1168 run: impl FnOnce() -> Result<(), Box<dyn std::error::Error>>,
1169 ) -> Result<Duration, Box<dyn std::error::Error>> {
1170 let started_at = current_unix_timestamp_label()?;
1171 let started = Instant::now();
1172 match run() {
1173 Ok(()) => {
1174 let duration = started.elapsed();
1175 let receipt = self.with_execution_context(install_deployment_truth_phase_receipt(
1176 self.check,
1177 phase,
1178 started_at,
1179 Some(current_unix_timestamp_label()?),
1180 attempted_action,
1181 crate::deployment_truth::ObservationStatusV1::Observed,
1182 evidence,
1183 ));
1184 self.write_receipt(&receipt)?;
1185 Ok(duration)
1186 }
1187 Err(err) => {
1188 self.try_write_failed_phase_receipt(
1189 phase,
1190 started_at,
1191 attempted_action,
1192 evidence,
1193 err.as_ref(),
1194 );
1195 Err(err)
1196 }
1197 }
1198 }
1199
1200 fn with_execution_context(self, receipt: DeploymentReceiptV1) -> DeploymentReceiptV1 {
1201 match self.execution_context {
1202 Some(context) => receipt_with_execution_context(receipt, context),
1203 None => receipt,
1204 }
1205 }
1206
1207 fn write_receipt(
1208 self,
1209 receipt: &DeploymentReceiptV1,
1210 ) -> Result<PathBuf, Box<dyn std::error::Error>> {
1211 let path = write_install_deployment_truth_receipt(
1212 self.icp_root,
1213 self.network,
1214 self.fleet_name,
1215 receipt,
1216 )?;
1217 println!("Deployment truth receipt JSON: {}", path.display());
1218 Ok(path)
1219 }
1220
1221 fn try_write_failed_phase_receipt(
1222 self,
1223 phase: &str,
1224 started_at: String,
1225 attempted_action: &str,
1226 evidence: Vec<String>,
1227 err: &dyn std::error::Error,
1228 ) {
1229 let receipt = install_deployment_truth_phase_receipt_with_result(
1230 self.check,
1231 PhaseReceiptInput {
1232 phase,
1233 started_at,
1234 finished_at: Some(
1235 current_unix_timestamp_label().unwrap_or_else(|_| "unknown".to_string()),
1236 ),
1237 attempted_action,
1238 status: crate::deployment_truth::ObservationStatusV1::Inconclusive,
1239 evidence,
1240 role_phase_receipts: Vec::new(),
1241 operation_status: DeploymentExecutionStatusV1::FailedAfterMutation,
1242 command_result: DeploymentCommandResultV1::Failed {
1243 code: format!("{phase}_failed"),
1244 message: err.to_string(),
1245 },
1246 },
1247 );
1248 let receipt = self.with_execution_context(receipt);
1249 if let Err(write_err) = self.write_receipt(&receipt) {
1250 eprintln!("Deployment truth receipt JSON write failed: {write_err}");
1251 }
1252 }
1253}
1254
1255pub fn check_install_deployment_truth(
1258 options: &InstallRootOptions,
1259 observed_at: impl Into<String>,
1260) -> Result<DeploymentCheckV1, Box<dyn std::error::Error>> {
1261 let inputs = resolve_current_install_truth_inputs(options)?;
1262 current_install_deployment_truth_check_at(
1263 options,
1264 &inputs.workspace_root,
1265 &inputs.icp_root,
1266 &inputs.config_path,
1267 &inputs.fleet_name,
1268 observed_at.into(),
1269 )
1270}
1271
1272pub fn check_install_execution_preflight(
1278 options: &InstallRootOptions,
1279 observed_at: impl Into<String>,
1280) -> Result<DeploymentExecutionPreflightV1, Box<dyn std::error::Error>> {
1281 let inputs = resolve_current_install_truth_inputs(options)?;
1282 let check = current_install_deployment_truth_check_at(
1283 options,
1284 &inputs.workspace_root,
1285 &inputs.icp_root,
1286 &inputs.config_path,
1287 &inputs.fleet_name,
1288 observed_at.into(),
1289 )?;
1290 let execution_context = current_install_execution_context(
1291 &inputs.workspace_root,
1292 &inputs.icp_root,
1293 &options.network,
1294 );
1295 let executor = CurrentCliDeploymentExecutor::new(
1296 execution_context.workspace_root,
1297 execution_context.icp_root,
1298 execution_context.artifact_roots,
1299 );
1300 let preflight = deployment_execution_preflight_from_check(
1301 &check,
1302 &executor,
1303 CURRENT_INSTALL_REQUIRED_CAPABILITIES,
1304 );
1305 validate_deployment_execution_preflight_for_check(&check, &preflight)?;
1306 Ok(preflight)
1307}
1308
1309fn resolve_current_install_truth_inputs(
1310 options: &InstallRootOptions,
1311) -> Result<CurrentInstallTruthInputs, Box<dyn std::error::Error>> {
1312 let workspace_root = workspace_root()?;
1313 let icp_root = match &options.icp_root {
1314 Some(path) => path.canonicalize()?,
1315 None => icp_root()?,
1316 };
1317 let config_path = resolve_install_config_path(
1318 &icp_root,
1319 options.config_path.as_deref(),
1320 options.interactive_config_selection,
1321 )?;
1322 let fleet_name = configured_fleet_name(&config_path)?;
1323 validate_expected_fleet_name(options.expected_fleet.as_deref(), &fleet_name, &config_path)?;
1324 validate_fleet_name(&fleet_name)?;
1325 Ok(CurrentInstallTruthInputs {
1326 workspace_root,
1327 icp_root,
1328 config_path,
1329 fleet_name,
1330 })
1331}
1332
1333fn current_install_deployment_truth_check_at(
1334 options: &InstallRootOptions,
1335 workspace_root: &Path,
1336 icp_root: &Path,
1337 config_path: &Path,
1338 fleet_name: &str,
1339 observed_at: String,
1340) -> Result<DeploymentCheckV1, Box<dyn std::error::Error>> {
1341 if let Some(plan) = &options.deployment_plan_override {
1342 validate_current_install_plan_override(plan, &options.network, fleet_name)?;
1343 return current_install_deployment_truth_check_for_plan(
1344 plan,
1345 workspace_root,
1346 icp_root,
1347 config_path,
1348 fleet_name,
1349 observed_at,
1350 &options.network,
1351 );
1352 }
1353
1354 let build_profile = options
1355 .build_profile
1356 .unwrap_or_else(CanisterBuildProfile::current)
1357 .target_dir_name()
1358 .to_string();
1359
1360 check_local_deployment(&LocalDeploymentCheckRequest {
1361 deployment_name: fleet_name.to_string(),
1362 network: options.network.clone(),
1363 workspace_root: workspace_root.to_path_buf(),
1364 icp_root: icp_root.to_path_buf(),
1365 config_path: Some(config_path.to_path_buf()),
1366 observed_at,
1367 runtime_variant: options.network.clone(),
1368 build_profile,
1369 })
1370 .map_err(Into::into)
1371}
1372
1373fn current_install_deployment_truth_check_for_plan(
1374 plan: &DeploymentPlanV1,
1375 workspace_root: &Path,
1376 icp_root: &Path,
1377 config_path: &Path,
1378 fleet_name: &str,
1379 observed_at: String,
1380 network: &str,
1381) -> Result<DeploymentCheckV1, Box<dyn std::error::Error>> {
1382 let inventory = collect_local_deployment_inventory(&LocalInventoryRequest {
1383 deployment_name: fleet_name.to_string(),
1384 network: network.to_string(),
1385 workspace_root: workspace_root.to_path_buf(),
1386 icp_root: icp_root.to_path_buf(),
1387 config_path: Some(config_path.to_path_buf()),
1388 observed_at,
1389 })?;
1390 let diff = compare_plan_to_inventory(plan, &inventory);
1391 let report = safety_report_from_diff(
1392 format!("local:{network}:{fleet_name}:report"),
1393 Some(format!("local:{network}:{fleet_name}:diff")),
1394 &diff,
1395 );
1396
1397 Ok(DeploymentCheckV1 {
1398 schema_version: crate::deployment_truth::DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1399 check_id: format!("local:{network}:{fleet_name}:check"),
1400 plan: plan.clone(),
1401 inventory,
1402 diff,
1403 report,
1404 })
1405}
1406
1407fn validate_current_install_plan_override(
1408 plan: &DeploymentPlanV1,
1409 network: &str,
1410 fleet_name: &str,
1411) -> Result<(), Box<dyn std::error::Error>> {
1412 if plan.schema_version != crate::deployment_truth::DEPLOYMENT_TRUTH_SCHEMA_VERSION {
1413 return Err(format!(
1414 "deployment plan schema mismatch: expected {}, found {}",
1415 crate::deployment_truth::DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1416 plan.schema_version
1417 )
1418 .into());
1419 }
1420 if plan.deployment_identity.network != network {
1421 return Err(format!(
1422 "deployment plan network mismatch: install network {network}, plan network {}",
1423 plan.deployment_identity.network
1424 )
1425 .into());
1426 }
1427 if plan.deployment_identity.deployment_name != fleet_name && plan.fleet_template != fleet_name {
1428 return Err(format!(
1429 "deployment plan target mismatch: install fleet {fleet_name}, plan deployment {} / template {}",
1430 plan.deployment_identity.deployment_name, plan.fleet_template
1431 )
1432 .into());
1433 }
1434 Ok(())
1435}
1436
1437fn current_install_execution_context(
1438 workspace_root: &Path,
1439 icp_root: &Path,
1440 network: &str,
1441) -> DeploymentExecutionContextV1 {
1442 CurrentCliDeploymentExecutor::new(
1443 Some(workspace_root.display().to_string()),
1444 Some(icp_root.display().to_string()),
1445 current_install_artifact_roots(icp_root, network),
1446 )
1447 .execution_context()
1448}
1449
1450fn ensure_current_install_executor_capabilities(
1451 execution_context: &DeploymentExecutionContextV1,
1452) -> Result<(), Box<dyn std::error::Error>> {
1453 let missing = current_install_executor_missing_capabilities(execution_context);
1454 if missing.is_empty() {
1455 return Ok(());
1456 }
1457
1458 Err(format!(
1459 "current install executor backend {:?} is missing required capabilities: {missing:?}",
1460 execution_context.backend
1461 )
1462 .into())
1463}
1464
1465fn current_install_executor_missing_capabilities(
1466 execution_context: &DeploymentExecutionContextV1,
1467) -> Vec<DeploymentExecutorCapabilityV1> {
1468 missing_executor_capabilities(
1469 &execution_context.backend_capabilities,
1470 CURRENT_INSTALL_REQUIRED_CAPABILITIES,
1471 )
1472}
1473
1474fn current_install_artifact_roots(icp_root: &Path, network: &str) -> Vec<String> {
1475 let planned_root = planned_build_artifact_root(icp_root);
1476 let mut roots = vec![planned_root.display().to_string()];
1477 if let Ok(resolved_root) = resolve_artifact_root(icp_root, network)
1478 && resolved_root != planned_root
1479 {
1480 roots.push(resolved_root.display().to_string());
1481 }
1482 roots
1483}
1484
1485fn run_install_deployment_truth_safety_gate(
1486 options: &InstallRootOptions,
1487 workspace_root: &Path,
1488 icp_root: &Path,
1489 config_path: &Path,
1490 fleet_name: &str,
1491 execution_context: &DeploymentExecutionContextV1,
1492) -> Result<DeploymentCheckV1, Box<dyn std::error::Error>> {
1493 let truth_gate_started_at = current_unix_timestamp_label()?;
1494 let deployment_truth_check = current_install_deployment_truth_check_at(
1495 options,
1496 workspace_root,
1497 icp_root,
1498 config_path,
1499 fleet_name,
1500 truth_gate_started_at.clone(),
1501 )?;
1502 let artifact_gate_receipt = artifact_gate_phase_receipt(
1503 &deployment_truth_check,
1504 truth_gate_started_at.clone(),
1505 Some(current_unix_timestamp_label()?),
1506 );
1507 let role_receipts = artifact_gate_role_phase_receipts(&deployment_truth_check);
1508 let deployment_receipt = receipt_with_execution_context(
1509 install_deployment_truth_gate_receipt(
1510 &deployment_truth_check,
1511 truth_gate_started_at,
1512 vec![artifact_gate_receipt],
1513 role_receipts,
1514 ),
1515 execution_context,
1516 );
1517 let receipt_write = write_install_deployment_truth_receipt(
1518 icp_root,
1519 &options.network,
1520 fleet_name,
1521 &deployment_receipt,
1522 );
1523 match &receipt_write {
1524 Ok(path) => println!("Deployment truth receipt JSON: {}", path.display()),
1525 Err(err) => eprintln!("Deployment truth receipt JSON write failed: {err}"),
1526 }
1527 print_install_deployment_truth_gate(&deployment_truth_check, &deployment_receipt);
1528 enforce_install_deployment_truth_gate(&deployment_truth_check)?;
1529 receipt_write?;
1530 write_current_install_execution_preflight_receipt(
1531 icp_root,
1532 &options.network,
1533 fleet_name,
1534 &deployment_truth_check,
1535 execution_context,
1536 )?;
1537 Ok(deployment_truth_check)
1538}
1539
1540fn enforce_install_deployment_truth_gate(
1541 check: &DeploymentCheckV1,
1542) -> Result<(), Box<dyn std::error::Error>> {
1543 let blockers = install_deployment_truth_gate_blockers(check);
1544 if blockers.is_empty() {
1545 return Ok(());
1546 }
1547
1548 let details = blockers
1549 .iter()
1550 .map(|finding| deployment_truth_finding_label(finding))
1551 .collect::<Vec<_>>()
1552 .join("; ");
1553 Err(format!("deployment truth safety gate blocked install: {details}").into())
1554}
1555
1556fn install_deployment_truth_gate_blockers(check: &DeploymentCheckV1) -> Vec<&SafetyFindingV1> {
1557 check.report.hard_failures.iter().collect()
1558}
1559
1560fn print_install_deployment_truth_gate(check: &DeploymentCheckV1, receipt: &DeploymentReceiptV1) {
1561 for line in install_deployment_truth_gate_lines(check, receipt) {
1562 println!("{line}");
1563 }
1564}
1565
1566fn install_deployment_truth_gate_lines(
1567 check: &DeploymentCheckV1,
1568 receipt: &DeploymentReceiptV1,
1569) -> Vec<String> {
1570 let mut lines = vec![
1571 format!("Deployment truth: {}", check.report.summary),
1572 format!(
1573 "Deployment truth receipt: operation={} status={:?}",
1574 receipt.operation_id, receipt.operation_status
1575 ),
1576 ];
1577 for phase_receipt in &receipt.phase_receipts {
1578 lines.push(format!(
1579 "Deployment truth phase receipt: phase={} postcondition={:?}",
1580 phase_receipt.phase, phase_receipt.verified_postcondition.status
1581 ));
1582 }
1583 if !receipt.role_phase_receipts.is_empty() {
1584 lines.push(format!(
1585 "Deployment truth role receipts: {}",
1586 receipt.role_phase_receipts.len()
1587 ));
1588 }
1589 for role_receipt in &receipt.role_phase_receipts {
1590 lines.push(format!(
1591 "Deployment truth role receipt: phase={} role={} result={:?}",
1592 role_receipt.phase, role_receipt.role, role_receipt.result
1593 ));
1594 }
1595
1596 if !check.report.hard_failures.is_empty() {
1597 lines.push(format!(
1598 "Deployment truth hard failures: {}",
1599 check.report.hard_failures.len()
1600 ));
1601 }
1602 for finding in install_deployment_truth_gate_blockers(check) {
1603 lines.push(format!(
1604 "Deployment truth blocker: {}",
1605 deployment_truth_finding_label(finding)
1606 ));
1607 }
1608 if !check.report.warnings.is_empty() {
1609 lines.push(format!(
1610 "Deployment truth warnings: {}",
1611 check.report.warnings.len()
1612 ));
1613 }
1614 for finding in &check.report.warnings {
1615 lines.push(format!(
1616 "Deployment truth warning: {}",
1617 deployment_truth_finding_label(finding)
1618 ));
1619 }
1620 lines
1621}
1622
1623fn install_deployment_truth_gate_receipt(
1624 check: &DeploymentCheckV1,
1625 started_at: String,
1626 phase_receipts: Vec<crate::deployment_truth::PhaseReceiptV1>,
1627 role_phase_receipts: Vec<crate::deployment_truth::RolePhaseReceiptV1>,
1628) -> DeploymentReceiptV1 {
1629 let blockers = install_deployment_truth_gate_blockers(check);
1630 let (operation_status, command_result) = if blockers.is_empty() {
1631 (
1632 DeploymentExecutionStatusV1::Complete,
1633 DeploymentCommandResultV1::Succeeded,
1634 )
1635 } else {
1636 (
1637 DeploymentExecutionStatusV1::FailedBeforeMutation,
1638 DeploymentCommandResultV1::Failed {
1639 code: "deployment_truth_blocked".to_string(),
1640 message: check.report.summary.clone(),
1641 },
1642 )
1643 };
1644 deployment_receipt_from_check_with_status(
1645 check,
1646 format!("{}:materialize_artifacts", check.check_id),
1647 operation_status,
1648 started_at,
1649 Some(current_unix_timestamp_label().unwrap_or_else(|_| "unknown".to_string())),
1650 phase_receipts,
1651 role_phase_receipts,
1652 command_result,
1653 )
1654}
1655
1656fn write_current_install_execution_preflight_receipt(
1657 icp_root: &Path,
1658 network: &str,
1659 fleet_name: &str,
1660 check: &DeploymentCheckV1,
1661 execution_context: &DeploymentExecutionContextV1,
1662) -> Result<PathBuf, Box<dyn std::error::Error>> {
1663 let started_at = current_unix_timestamp_label()?;
1664 let executor = CurrentCliDeploymentExecutor::new(
1665 execution_context.workspace_root.clone(),
1666 execution_context.icp_root.clone(),
1667 execution_context.artifact_roots.clone(),
1668 );
1669 let preflight = deployment_execution_preflight_from_check(
1670 check,
1671 &executor,
1672 CURRENT_INSTALL_REQUIRED_CAPABILITIES,
1673 );
1674 validate_deployment_execution_preflight_for_check(check, &preflight)?;
1675 let blockers = preflight.blockers.clone();
1676 let (operation_status, command_result) = if blockers.is_empty() {
1677 (
1678 DeploymentExecutionStatusV1::Complete,
1679 DeploymentCommandResultV1::Succeeded,
1680 )
1681 } else {
1682 (
1683 DeploymentExecutionStatusV1::FailedBeforeMutation,
1684 DeploymentCommandResultV1::Failed {
1685 code: "execution_preflight_blocked".to_string(),
1686 message: "deployment execution preflight blocked current install".to_string(),
1687 },
1688 )
1689 };
1690 let finished_at = current_unix_timestamp_label()?;
1691 let receipt = receipt_with_execution_context(
1692 deployment_receipt_from_check_with_status(
1693 check,
1694 format!("{}:execution_preflight", check.check_id),
1695 operation_status,
1696 started_at.clone(),
1697 Some(finished_at.clone()),
1698 vec![phase_receipt(
1699 "execution_preflight",
1700 started_at,
1701 Some(finished_at),
1702 "validate deployment plan, authority, and executor capability readiness",
1703 crate::deployment_truth::ObservationStatusV1::Observed,
1704 current_install_execution_preflight_evidence(&preflight),
1705 )],
1706 Vec::new(),
1707 command_result,
1708 ),
1709 execution_context,
1710 );
1711 let path = write_install_deployment_truth_receipt(icp_root, network, fleet_name, &receipt)?;
1712 println!("Deployment truth receipt JSON: {}", path.display());
1713 if !blockers.is_empty() {
1714 let details = blockers
1715 .iter()
1716 .map(deployment_truth_finding_label)
1717 .collect::<Vec<_>>()
1718 .join("; ");
1719 return Err(format!("deployment execution preflight blocked install: {details}").into());
1720 }
1721 Ok(path)
1722}
1723
1724struct StageReleaseSetOperation<'a> {
1725 icp_root: &'a Path,
1726 network: &'a str,
1727 root_canister_id: &'a str,
1728 manifest_path: &'a Path,
1729 manifest: RootReleaseSetManifest,
1730}
1731
1732impl<'a> StageReleaseSetOperation<'a> {
1733 const fn new(
1734 icp_root: &'a Path,
1735 network: &'a str,
1736 root_canister_id: &'a str,
1737 manifest_path: &'a Path,
1738 manifest: RootReleaseSetManifest,
1739 ) -> Self {
1740 Self {
1741 icp_root,
1742 network,
1743 root_canister_id,
1744 manifest_path,
1745 manifest,
1746 }
1747 }
1748}
1749
1750impl InstallPhaseOperation for StageReleaseSetOperation<'_> {
1751 fn phase(&self) -> &'static str {
1752 "stage_release_set"
1753 }
1754
1755 fn attempted_action(&self) -> &'static str {
1756 "stage root release set"
1757 }
1758
1759 fn evidence(&self) -> Vec<String> {
1760 current_install_staging_evidence(self.root_canister_id, self.manifest_path, &self.manifest)
1761 }
1762
1763 fn execute(&self) -> Result<(), Box<dyn std::error::Error>> {
1764 stage_root_release_set(
1765 self.icp_root,
1766 self.network,
1767 self.root_canister_id,
1768 &self.manifest,
1769 )
1770 }
1771}
1772
1773fn current_install_execution_preflight_evidence(
1774 preflight: &crate::deployment_truth::DeploymentExecutionPreflightV1,
1775) -> Vec<String> {
1776 let mut evidence = vec![
1777 format!("execution_preflight_status:{:?}", preflight.status),
1778 format!("authority_plan:{}", preflight.authority_plan_id),
1779 format!("planned_phases:{}", preflight.planned_phases.len()),
1780 format!(
1781 "required_capabilities:{}",
1782 preflight.required_capabilities.len()
1783 ),
1784 format!(
1785 "missing_capabilities:{}",
1786 preflight.missing_capabilities.len()
1787 ),
1788 format!("blockers:{}", preflight.blockers.len()),
1789 ];
1790 evidence.extend(
1791 preflight
1792 .missing_capabilities
1793 .iter()
1794 .map(|capability| format!("missing_capability:{capability:?}")),
1795 );
1796 evidence.extend(
1797 preflight
1798 .blockers
1799 .iter()
1800 .map(|finding| format!("blocker:{}:{}", finding.code, finding.message)),
1801 );
1802 evidence
1803}
1804
1805fn current_install_staging_evidence(
1806 root_canister_id: &str,
1807 manifest_path: &Path,
1808 manifest: &RootReleaseSetManifest,
1809) -> Vec<String> {
1810 let mut evidence = vec![
1811 format!("root_canister:{root_canister_id}"),
1812 format!("manifest_path:{}", manifest_path.display()),
1813 format!("release_version:{}", manifest.release_version),
1814 ];
1815 let staging_receipts = current_install_staging_receipts(root_canister_id, manifest);
1816 evidence.extend(staging_receipt_evidence(&staging_receipts));
1817 evidence
1818}
1819
1820fn current_install_staging_receipts(
1821 root_canister_id: &str,
1822 manifest: &RootReleaseSetManifest,
1823) -> Vec<StagingReceiptV1> {
1824 manifest
1825 .entries
1826 .iter()
1827 .map(|entry| StagingReceiptV1 {
1828 schema_version: crate::deployment_truth::DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1829 role: entry.role.clone(),
1830 artifact_identity: format!(
1831 "{}:{}:{}",
1832 entry.template_id, manifest.release_version, entry.payload_sha256_hex
1833 ),
1834 transport: ArtifactTransportV1::WasmStore,
1835 wasm_store_locator: Some(format!("root:{root_canister_id}:bootstrap")),
1836 prepared_chunk_hashes: entry.chunk_sha256_hex.clone(),
1837 published_chunk_count: entry.chunk_sha256_hex.len(),
1838 verified_postcondition: crate::deployment_truth::VerifiedPostconditionV1 {
1839 status: ObservationStatusV1::Observed,
1840 evidence: vec![
1841 format!("payload_sha256:{}", entry.payload_sha256_hex),
1842 format!("payload_size_bytes:{}", entry.payload_size_bytes),
1843 format!("chunk_size_bytes:{}", entry.chunk_size_bytes),
1844 format!("chunk_count:{}", entry.chunk_sha256_hex.len()),
1845 ],
1846 },
1847 })
1848 .collect()
1849}
1850
1851fn install_deployment_truth_phase_receipt(
1852 check: &DeploymentCheckV1,
1853 phase: &str,
1854 started_at: String,
1855 finished_at: Option<String>,
1856 attempted_action: &str,
1857 status: crate::deployment_truth::ObservationStatusV1,
1858 evidence: Vec<String>,
1859) -> DeploymentReceiptV1 {
1860 install_deployment_truth_phase_receipt_with_result(
1861 check,
1862 PhaseReceiptInput {
1863 phase,
1864 started_at,
1865 finished_at,
1866 attempted_action,
1867 status,
1868 evidence,
1869 role_phase_receipts: Vec::new(),
1870 operation_status: DeploymentExecutionStatusV1::Complete,
1871 command_result: DeploymentCommandResultV1::Succeeded,
1872 },
1873 )
1874}
1875
1876fn install_deployment_truth_phase_receipt_with_result(
1877 check: &DeploymentCheckV1,
1878 input: PhaseReceiptInput<'_>,
1879) -> DeploymentReceiptV1 {
1880 deployment_receipt_from_check_with_status(
1881 check,
1882 format!("{}:{}", check.check_id, input.phase),
1883 input.operation_status,
1884 input.started_at.clone(),
1885 input.finished_at.clone(),
1886 vec![phase_receipt(
1887 input.phase,
1888 input.started_at,
1889 input.finished_at,
1890 input.attempted_action,
1891 input.status,
1892 input.evidence,
1893 )],
1894 input.role_phase_receipts,
1895 input.command_result,
1896 )
1897}
1898
1899fn receipt_with_execution_context(
1900 mut receipt: DeploymentReceiptV1,
1901 execution_context: &DeploymentExecutionContextV1,
1902) -> DeploymentReceiptV1 {
1903 receipt.execution_context = Some(execution_context.clone());
1904 receipt
1905}
1906
1907struct PhaseReceiptInput<'a> {
1908 phase: &'a str,
1909 started_at: String,
1910 finished_at: Option<String>,
1911 attempted_action: &'a str,
1912 status: crate::deployment_truth::ObservationStatusV1,
1913 evidence: Vec<String>,
1914 role_phase_receipts: Vec<crate::deployment_truth::RolePhaseReceiptV1>,
1915 operation_status: DeploymentExecutionStatusV1,
1916 command_result: DeploymentCommandResultV1,
1917}
1918
1919fn write_install_deployment_truth_receipt(
1920 icp_root: &Path,
1921 network: &str,
1922 fleet_name: &str,
1923 receipt: &DeploymentReceiptV1,
1924) -> Result<PathBuf, Box<dyn std::error::Error>> {
1925 let path = install_deployment_truth_receipt_path(icp_root, network, fleet_name, receipt)?;
1926 if let Some(parent) = path.parent() {
1927 fs::create_dir_all(parent)?;
1928 }
1929 let mut bytes = serde_json::to_vec_pretty(receipt)?;
1930 bytes.push(b'\n');
1931 fs::write(&path, bytes)?;
1932 Ok(path)
1933}
1934
1935fn write_artifact_promotion_execution_receipt(
1936 icp_root: &Path,
1937 network: &str,
1938 fleet_name: &str,
1939 receipt: &ArtifactPromotionExecutionReceiptV1,
1940) -> Result<PathBuf, Box<dyn std::error::Error>> {
1941 let path = artifact_promotion_execution_receipt_path(icp_root, network, fleet_name, receipt)?;
1942 if let Some(parent) = path.parent() {
1943 fs::create_dir_all(parent)?;
1944 }
1945 let mut bytes = serde_json::to_vec_pretty(receipt)?;
1946 bytes.push(b'\n');
1947 fs::write(&path, bytes)?;
1948 Ok(path)
1949}
1950
1951fn artifact_promotion_execution_receipt_path(
1952 icp_root: &Path,
1953 network: &str,
1954 fleet_name: &str,
1955 receipt: &ArtifactPromotionExecutionReceiptV1,
1956) -> Result<PathBuf, Box<dyn std::error::Error>> {
1957 validate_network_name(network)?;
1958 validate_fleet_name(fleet_name)?;
1959 let file_stem = format!(
1960 "{}-{}",
1961 safe_deployment_truth_path_label(&receipt.started_at),
1962 safe_deployment_truth_path_label(&receipt.receipt_id)
1963 );
1964 Ok(
1965 artifact_promotion_execution_receipts_dir(icp_root, network, fleet_name)?
1966 .join(format!("{file_stem}.json")),
1967 )
1968}
1969
1970fn artifact_promotion_execution_receipts_dir(
1971 icp_root: &Path,
1972 network: &str,
1973 fleet_name: &str,
1974) -> Result<PathBuf, Box<dyn std::error::Error>> {
1975 validate_network_name(network)?;
1976 validate_fleet_name(fleet_name)?;
1977 Ok(icp_root
1978 .join(".canic")
1979 .join(network)
1980 .join("artifact-promotion-execution-receipts")
1981 .join(fleet_name))
1982}
1983
1984fn install_deployment_truth_receipt_path(
1985 icp_root: &Path,
1986 network: &str,
1987 fleet_name: &str,
1988 receipt: &DeploymentReceiptV1,
1989) -> Result<PathBuf, Box<dyn std::error::Error>> {
1990 validate_network_name(network)?;
1991 validate_fleet_name(fleet_name)?;
1992 let file_stem = format!(
1993 "{}-{}",
1994 safe_deployment_truth_path_label(&receipt.started_at),
1995 safe_deployment_truth_path_label(&receipt.operation_id)
1996 );
1997 Ok(
1998 install_deployment_truth_receipts_dir(icp_root, network, fleet_name)?
1999 .join(format!("{file_stem}.json")),
2000 )
2001}
2002
2003pub fn latest_deployment_truth_receipt_path_from_root(
2005 icp_root: &Path,
2006 network: &str,
2007 fleet_name: &str,
2008) -> Result<Option<PathBuf>, Box<dyn std::error::Error>> {
2009 let dir = install_deployment_truth_receipts_dir(icp_root, network, fleet_name)?;
2010 if !dir.is_dir() {
2011 return Ok(None);
2012 }
2013
2014 let mut latest = None;
2015 for entry in fs::read_dir(dir)? {
2016 let path = entry?.path();
2017 if !path.is_file()
2018 || path
2019 .extension()
2020 .is_none_or(|ext| !ext.eq_ignore_ascii_case("json"))
2021 {
2022 continue;
2023 }
2024 if latest.as_ref().is_none_or(|current| path > *current) {
2025 latest = Some(path);
2026 }
2027 }
2028 Ok(latest)
2029}
2030
2031fn install_deployment_truth_receipts_dir(
2032 icp_root: &Path,
2033 network: &str,
2034 fleet_name: &str,
2035) -> Result<PathBuf, Box<dyn std::error::Error>> {
2036 validate_network_name(network)?;
2037 validate_fleet_name(fleet_name)?;
2038 Ok(icp_root
2039 .join(".canic")
2040 .join(network)
2041 .join("deployment-receipts")
2042 .join(fleet_name))
2043}
2044
2045fn safe_deployment_truth_path_label(value: &str) -> String {
2046 let label = value
2047 .chars()
2048 .map(|ch| {
2049 if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
2050 ch
2051 } else {
2052 '_'
2053 }
2054 })
2055 .collect::<String>();
2056 if label.is_empty() {
2057 "unknown".to_string()
2058 } else {
2059 label
2060 }
2061}
2062
2063fn deployment_truth_finding_label(finding: &SafetyFindingV1) -> String {
2064 let subject = finding
2065 .subject
2066 .as_ref()
2067 .map_or_else(|| "<none>".to_string(), Clone::clone);
2068 format!(
2069 "{}:{}:{}: {}",
2070 deployment_truth_finding_source(&finding.code),
2071 finding.code,
2072 subject,
2073 finding.message
2074 )
2075}
2076
2077fn deployment_truth_finding_source(code: &str) -> &'static str {
2078 match code {
2079 "plan_assumption" => "plan",
2080 "observation_gap" => "inventory",
2081 _ => "diff",
2082 }
2083}
2084
2085fn validate_expected_fleet_name(
2086 expected: Option<&str>,
2087 actual: &str,
2088 config_path: &Path,
2089) -> Result<(), Box<dyn std::error::Error>> {
2090 let Some(expected) = expected else {
2091 return Ok(());
2092 };
2093 if expected == actual {
2094 return Ok(());
2095 }
2096 Err(format!(
2097 "install requested fleet {expected}, but {} declares [fleet].name = {actual:?}",
2098 config_path.display()
2099 )
2100 .into())
2101}
2102
2103fn ensure_root_canister_id(
2104 icp_root: &Path,
2105 network: &str,
2106 root_canister: &str,
2107 config_path: &Path,
2108) -> Result<String, Box<dyn std::error::Error>> {
2109 if Principal::from_text(root_canister).is_ok() {
2110 return Ok(root_canister.to_string());
2111 }
2112
2113 match resolve_root_canister_id(icp_root, network, root_canister) {
2114 Ok(canister_id) => return Ok(canister_id),
2115 Err(err) if !is_missing_canister_id_error(&err.to_string()) => return Err(err),
2116 Err(_) => {}
2117 }
2118
2119 let mut create = icp_canister_command_in_network(icp_root);
2120 add_create_root_target(&mut create, root_canister);
2121 add_local_root_create_cycles_arg(&mut create, config_path, network)?;
2122 add_icp_environment_target(&mut create, network);
2123 let output = run_command_stdout(&mut create)?;
2124 if let Some(canister_id) = parse_created_canister_id(&output) {
2125 return Ok(canister_id);
2126 }
2127
2128 resolve_root_canister_id(icp_root, network, root_canister).map_err(|_| {
2129 format!(
2130 "created root canister target '{root_canister}', but ICP CLI still has no canister ID for environment '{network}' under ICP root {}\nExpected project-local state under {}/.icp/{network}. If another foreground replica is reachable, stop it and restart with `canic replica start --background` from this Canic project.",
2131 icp_root.display(),
2132 icp_root.display(),
2133 )
2134 .into()
2135 })
2136}
2137
2138fn parse_created_canister_id(output: &str) -> Option<String> {
2139 if let Ok(value) = serde_json::from_str::<JsonValue>(output) {
2140 return parse_canister_id_json(&value);
2141 }
2142
2143 output
2144 .lines()
2145 .map(str::trim)
2146 .find(|line| Principal::from_text(*line).is_ok())
2147 .map(ToString::to_string)
2148}
2149
2150fn parse_canister_id_json(value: &JsonValue) -> Option<String> {
2151 match value {
2152 JsonValue::String(text) if Principal::from_text(text).is_ok() => Some(text.clone()),
2153 JsonValue::Array(values) => values.iter().find_map(parse_canister_id_json),
2154 JsonValue::Object(object) => ["canister_id", "id", "principal"]
2155 .iter()
2156 .filter_map(|key| object.get(*key))
2157 .find_map(parse_canister_id_json),
2158 _ => None,
2159 }
2160}
2161
2162fn add_create_root_target(command: &mut Command, root_canister: &str) {
2163 if env::var_os(CANIC_ICP_LOCAL_NETWORK_URL_ENV).is_some() {
2164 command.args(["create", "--detached", "--json"]);
2165 } else {
2166 command.args(["create", root_canister, "--json"]);
2167 }
2168}
2169
2170fn is_missing_canister_id_error(message: &str) -> bool {
2171 message.contains("failed to lookup canister ID")
2172 || message.contains("could not find ID for canister")
2173 || message.contains("Canister ID is missing")
2174}
2175
2176fn reinstall_root_wasm(
2177 icp_root: &Path,
2178 network: &str,
2179 root_canister: &str,
2180 root_wasm: &Path,
2181) -> Result<(), Box<dyn std::error::Error>> {
2182 let mut install = icp_canister_command_in_network(icp_root);
2183 install.args(["install", root_canister, "--mode=reinstall", "-y", "--wasm"]);
2184 install.arg(root_wasm);
2185 install.args(["--args", &root_init_args(root_wasm)?]);
2186 add_icp_environment_target(&mut install, network);
2187 run_command(&mut install)
2188}
2189
2190fn root_init_args(root_wasm: &Path) -> Result<String, Box<dyn std::error::Error>> {
2191 let wasm = std::fs::read(root_wasm)?;
2192 Ok(format!(
2193 "(variant {{ PrimeWithModuleHash = {} }})",
2194 idl_blob(&wasm_hash(&wasm))
2195 ))
2196}
2197
2198fn idl_blob(bytes: &[u8]) -> String {
2199 let mut encoded = String::from("blob \"");
2200 for byte in bytes {
2201 use std::fmt::Write as _;
2202 let _ = write!(encoded, "\\{byte:02X}");
2203 }
2204 encoded.push('"');
2205 encoded
2206}
2207
2208fn build_install_state(
2210 options: &InstallRootOptions,
2211 workspace_root: &Path,
2212 icp_root: &Path,
2213 config_path: &Path,
2214 release_set_manifest_path: &Path,
2215 fleet_name: &str,
2216 root_canister_id: &str,
2217) -> Result<InstallState, Box<dyn std::error::Error>> {
2218 Ok(InstallState {
2219 schema_version: INSTALL_STATE_SCHEMA_VERSION,
2220 fleet: fleet_name.to_string(),
2221 installed_at_unix_secs: current_unix_secs()?,
2222 network: options.network.clone(),
2223 root_target: options.root_canister.clone(),
2224 root_canister_id: root_canister_id.to_string(),
2225 root_build_target: options.root_build_target.clone(),
2226 workspace_root: workspace_root.display().to_string(),
2227 icp_root: icp_root.display().to_string(),
2228 config_path: config_path.display().to_string(),
2229 release_set_manifest_path: release_set_manifest_path.display().to_string(),
2230 })
2231}
2232
2233fn resolve_root_canister_id(
2235 icp_root: &Path,
2236 network: &str,
2237 root_canister: &str,
2238) -> Result<String, Box<dyn std::error::Error>> {
2239 if Principal::from_text(root_canister).is_ok() {
2240 return Ok(root_canister.to_string());
2241 }
2242
2243 let mut command = icp_canister_command_in_network(icp_root);
2244 command.args(["status", root_canister, "--json"]);
2245 add_icp_environment_target(&mut command, network);
2246 let output = run_command_stdout(&mut command)?;
2247 parse_created_canister_id(&output).ok_or_else(|| {
2248 format!("could not parse root canister id from ICP status JSON output: {output}").into()
2249 })
2250}
2251
2252fn current_unix_secs() -> Result<u64, Box<dyn std::error::Error>> {
2254 Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())
2255}
2256
2257fn current_unix_timestamp_label() -> Result<String, Box<dyn std::error::Error>> {
2258 Ok(format!("unix:{}", current_unix_secs()?))
2259}
2260
2261fn run_canic_build_targets(
2263 network: &str,
2264 targets: &[String],
2265 build_profile: Option<CanisterBuildProfile>,
2266 config_path: &Path,
2267 icp_root: &Path,
2268) -> Result<(), Box<dyn std::error::Error>> {
2269 let _env = BuildEnvGuard::apply(network, config_path, icp_root);
2270 let profile = build_profile.unwrap_or_else(CanisterBuildProfile::current);
2271 if let Some(context) = current_workspace_build_context_once(profile)? {
2272 for line in context.lines() {
2273 println!("{line}");
2274 }
2275 println!("config: {}", config_path.display());
2276 println!(
2277 "artifacts: {}",
2278 planned_build_artifact_root(icp_root).display()
2279 );
2280 println!();
2281 }
2282
2283 fs::create_dir_all(planned_build_artifact_root(icp_root))?;
2284 println!("Building {} canisters", targets.len());
2285 println!();
2286 let headers = ["CANISTER", "PROGRESS", "WASM", "ELAPSED"];
2287 let planned_rows = targets
2288 .iter()
2289 .map(|target| {
2290 [
2291 target.clone(),
2292 progress_bar(targets.len(), targets.len(), 10),
2293 "000.00 MiB (gz 000.00 MiB)".to_string(),
2294 "0.00s".to_string(),
2295 ]
2296 })
2297 .collect::<Vec<_>>();
2298 let alignments = [
2299 ColumnAlign::Left,
2300 ColumnAlign::Left,
2301 ColumnAlign::Right,
2302 ColumnAlign::Right,
2303 ];
2304 let widths = table_widths(&headers, &planned_rows);
2305 println!("{}", render_table_row(&headers, &widths, &alignments));
2306 println!("{}", render_separator(&widths));
2307
2308 for (index, target) in targets.iter().enumerate() {
2309 let started_at = Instant::now();
2310 let output = build_current_workspace_canister_artifact(target, profile)
2311 .map_err(|err| format!("artifact build failed for {target}: {err}"))?;
2312 let elapsed = started_at.elapsed();
2313 let artifact_size = wasm_artifact_size(&output.wasm_path, &output.wasm_gz_path)?;
2314
2315 let row = [
2316 target.clone(),
2317 progress_bar(index + 1, targets.len(), 10),
2318 artifact_size,
2319 format!("{:.2}s", elapsed.as_secs_f64()),
2320 ];
2321 println!("{}", render_table_row(&row, &widths, &alignments));
2322 }
2323
2324 println!();
2325 Ok(())
2326}
2327
2328fn planned_build_artifact_root(icp_root: &Path) -> PathBuf {
2329 icp_root.join(".icp/local/canisters")
2330}
2331
2332fn wasm_artifact_size(
2333 wasm_path: &Path,
2334 wasm_gz_path: &Path,
2335) -> Result<String, Box<dyn std::error::Error>> {
2336 let wasm_bytes = Some(std::fs::metadata(wasm_path)?.len());
2337 let gzip_bytes = std::fs::metadata(wasm_gz_path)
2338 .ok()
2339 .map(|metadata| metadata.len());
2340 Ok(wasm_size_label(wasm_bytes, gzip_bytes))
2341}
2342
2343struct BuildEnvGuard {
2344 previous_network: Option<OsString>,
2345 previous_config_path: Option<OsString>,
2346 previous_icp_root: Option<OsString>,
2347 previous_local_network_url: Option<OsString>,
2348 previous_local_root_key: Option<OsString>,
2349}
2350
2351impl BuildEnvGuard {
2352 fn apply(network: &str, config_path: &Path, icp_root: &Path) -> Self {
2353 let guard = Self {
2354 previous_network: env::var_os("ICP_ENVIRONMENT"),
2355 previous_config_path: env::var_os("CANIC_CONFIG_PATH"),
2356 previous_icp_root: env::var_os("CANIC_ICP_ROOT"),
2357 previous_local_network_url: env::var_os(CANIC_ICP_LOCAL_NETWORK_URL_ENV),
2358 previous_local_root_key: env::var_os(CANIC_ICP_LOCAL_ROOT_KEY_ENV),
2359 };
2360 set_env("ICP_ENVIRONMENT", network);
2361 set_env("CANIC_CONFIG_PATH", config_path);
2362 set_env("CANIC_ICP_ROOT", icp_root);
2363 if let Some(target) = local_replica_icp_target(network, icp_root) {
2364 set_env(CANIC_ICP_LOCAL_NETWORK_URL_ENV, target.url);
2365 set_env(CANIC_ICP_LOCAL_ROOT_KEY_ENV, target.root_key);
2366 } else {
2367 remove_env(CANIC_ICP_LOCAL_NETWORK_URL_ENV);
2368 remove_env(CANIC_ICP_LOCAL_ROOT_KEY_ENV);
2369 }
2370 guard
2371 }
2372}
2373
2374impl Drop for BuildEnvGuard {
2375 fn drop(&mut self) {
2376 restore_env("ICP_ENVIRONMENT", self.previous_network.take());
2377 restore_env("CANIC_CONFIG_PATH", self.previous_config_path.take());
2378 restore_env("CANIC_ICP_ROOT", self.previous_icp_root.take());
2379 restore_env(
2380 CANIC_ICP_LOCAL_NETWORK_URL_ENV,
2381 self.previous_local_network_url.take(),
2382 );
2383 restore_env(
2384 CANIC_ICP_LOCAL_ROOT_KEY_ENV,
2385 self.previous_local_root_key.take(),
2386 );
2387 }
2388}
2389
2390struct LocalReplicaIcpTarget {
2391 url: String,
2392 root_key: String,
2393}
2394
2395fn local_replica_icp_target(network: &str, icp_root: &Path) -> Option<LocalReplicaIcpTarget> {
2396 if !replica_query::should_use_local_replica_query(Some(network)) {
2397 return None;
2398 }
2399 if icp_ping(icp_root, network).unwrap_or(false) {
2400 return None;
2401 }
2402 let root_key = replica_query::local_replica_root_key_from_root(Some(network), icp_root)
2403 .ok()
2404 .flatten()?;
2405 Some(LocalReplicaIcpTarget {
2406 url: replica_query::local_replica_endpoint_from_root(Some(network), icp_root),
2407 root_key,
2408 })
2409}
2410
2411fn set_env<K, V>(key: K, value: V)
2412where
2413 K: AsRef<std::ffi::OsStr>,
2414 V: AsRef<std::ffi::OsStr>,
2415{
2416 unsafe {
2419 env::set_var(key, value);
2420 }
2421}
2422
2423fn remove_env<K>(key: K)
2424where
2425 K: AsRef<std::ffi::OsStr>,
2426{
2427 unsafe {
2430 env::remove_var(key);
2431 }
2432}
2433
2434fn restore_env(key: &str, value: Option<OsString>) {
2435 if let Some(value) = value {
2437 set_env(key, value);
2438 } else {
2439 remove_env(key);
2440 }
2441}
2442
2443fn add_local_root_create_cycles_arg(
2444 command: &mut Command,
2445 config_path: &Path,
2446 network: &str,
2447) -> Result<(), Box<dyn std::error::Error>> {
2448 if network != "local" {
2449 return Ok(());
2450 }
2451
2452 let cycles = configured_local_root_create_cycles(config_path)?;
2453 command.args(["--cycles", &cycles.to_string()]);
2454 Ok(())
2455}
2456
2457fn ensure_local_root_min_cycles(
2458 icp_root: &Path,
2459 network: &str,
2460 root_canister: &str,
2461 phase: &str,
2462) -> Result<(), Box<dyn std::error::Error>> {
2463 if network != "local" {
2464 return Ok(());
2465 }
2466
2467 let current = query_root_cycle_balance(network, root_canister)?;
2468 if current >= LOCAL_ROOT_MIN_READY_CYCLES {
2469 return Ok(());
2470 }
2471
2472 let amount = LOCAL_ROOT_MIN_READY_CYCLES.saturating_sub(current);
2473 let mut command = icp_canister_command_in_network(icp_root);
2474 command
2475 .args(["top-up", "--amount"])
2476 .arg(amount.to_string())
2477 .arg(root_canister);
2478 add_icp_environment_target(&mut command, network);
2479 run_command(&mut command)?;
2480 println!(
2481 "Local root cycles ({phase}): topped up {} ({} -> {} target)",
2482 crate::format::cycles_tc(amount),
2483 crate::format::cycles_tc(current),
2484 crate::format::cycles_tc(LOCAL_ROOT_MIN_READY_CYCLES)
2485 );
2486 Ok(())
2487}
2488
2489fn query_root_cycle_balance(
2490 network: &str,
2491 root_canister: &str,
2492) -> Result<u128, Box<dyn std::error::Error>> {
2493 let output = icp_query_on_network(
2494 network,
2495 root_canister,
2496 protocol::CANIC_CYCLE_BALANCE,
2497 None,
2498 Some("json"),
2499 )?;
2500 parse_cycle_balance_response(&output).ok_or_else(|| {
2501 format!(
2502 "could not parse {root_canister} {} response: {output}",
2503 protocol::CANIC_CYCLE_BALANCE
2504 )
2505 .into()
2506 })
2507}
2508
2509fn progress_bar(current: usize, total: usize, width: usize) -> String {
2510 if total == 0 || width == 0 {
2511 return "[] 0/0".to_string();
2512 }
2513
2514 let filled = current.saturating_mul(width).div_ceil(total);
2515 let filled = filled.min(width);
2516 format!(
2517 "[{}{}] {current}/{total}",
2518 "#".repeat(filled),
2519 " ".repeat(width - filled)
2520 )
2521}
2522
2523fn ensure_icp_environment_ready(
2525 icp_root: &Path,
2526 network: &str,
2527) -> Result<(), Box<dyn std::error::Error>> {
2528 if icp_ping(icp_root, network)? {
2529 return Ok(());
2530 }
2531 if replica_query::should_use_local_replica_query(Some(network))
2532 && replica_query::local_replica_status_reachable_from_root(Some(network), icp_root)
2533 {
2534 println!(
2535 "Replica reachable via HTTP status endpoint even though ICP CLI reports network '{network}' stopped; continuing from ICP root {}.",
2536 icp_root.display()
2537 );
2538 return Ok(());
2539 }
2540
2541 Err(format!(
2542 "icp environment is not running for network '{network}'\nStart the target replica in another terminal with `canic replica start` and rerun."
2543 )
2544 .into())
2545}
2546
2547fn icp_ping(icp_root: &Path, network: &str) -> Result<bool, Box<dyn std::error::Error>> {
2549 Ok(icp::default_command_in(icp_root)
2550 .args(["network", "ping", network])
2551 .output()?
2552 .status
2553 .success())
2554}
2555
2556fn print_install_timing_summary(timings: &InstallTimingSummary, total: Duration) {
2557 println!("Install timing summary:");
2558 println!("{}", render_install_timing_summary(timings, total));
2559}
2560
2561fn render_install_timing_summary(timings: &InstallTimingSummary, total: Duration) -> String {
2562 let rows = [
2563 timing_row("create_canisters", timings.create_canisters),
2564 timing_row("build_all", timings.build_all),
2565 timing_row("emit_manifest", timings.emit_manifest),
2566 timing_row("install_root", timings.install_root),
2567 timing_row("fund_root", timings.fund_root),
2568 timing_row("stage_release_set", timings.stage_release_set),
2569 timing_row("resume_bootstrap", timings.resume_bootstrap),
2570 timing_row("wait_ready", timings.wait_ready),
2571 timing_row("finalize_root_funding", timings.finalize_root_funding),
2572 timing_row("total", total),
2573 ];
2574 render_table(
2575 &["PHASE", "ELAPSED"],
2576 &rows,
2577 &[ColumnAlign::Left, ColumnAlign::Right],
2578 )
2579}
2580
2581fn timing_row(label: &str, duration: Duration) -> [String; 2] {
2582 [label.to_string(), format!("{:.2}s", duration.as_secs_f64())]
2583}
2584
2585fn print_install_result_summary(network: &str, fleet: &str, state_path: &Path) {
2587 println!("Install result:");
2588 println!("{:<14} success", "status");
2589 println!("{:<14} {}", "fleet", fleet);
2590 println!("{:<14} {}", "install_state", state_path.display());
2591 println!(
2592 "{:<14} canic list {} --network {}",
2593 "smoke_check", fleet, network
2594 );
2595}
2596
2597fn run_command(command: &mut Command) -> Result<(), Box<dyn std::error::Error>> {
2599 icp::run_status(command).map_err(Into::into)
2600}
2601
2602fn run_command_stdout(command: &mut Command) -> Result<String, Box<dyn std::error::Error>> {
2604 icp::run_output(command).map_err(Into::into)
2605}
2606
2607fn icp_command_on_network(network: &str) -> Command {
2610 let mut command = icp::default_command();
2611 command.env("ICP_ENVIRONMENT", network);
2612 command
2613}
2614
2615fn icp_command_in_network(icp_root: &Path, network: &str) -> Command {
2617 let mut command = icp::default_command_in(icp_root);
2618 command.env("ICP_ENVIRONMENT", network);
2619 command
2620}
2621
2622fn icp_canister_command_in_network(icp_root: &Path) -> Command {
2624 let mut command = icp::default_command_in(icp_root);
2625 command.arg("canister");
2626 command
2627}
2628
2629fn add_icp_environment_target(command: &mut Command, network: &str) {
2630 icp::add_target_args(command, Some(network), None);
2631}