1use crate::{
2 canister_build::cache::DefaultCanisterBuildCacheCleanup,
3 deployment_truth::DeploymentReceiptV1,
4 fleet_install_input::{ResolvedFleetInstallInput, load_and_resolve_fleet_install_input},
5 fleet_install_plan::{
6 FleetInstallPlanRequest, PersistedFleetInstallPlan, compile_and_persist_fleet_install_plan,
7 },
8 network::resolve_canonical_network_id_from_root,
9 release_set::{AppConfigSnapshot, icp_root, workspace_root},
10};
11use config_selection::resolve_install_config_path;
12use std::{
13 fmt,
14 path::{Path, PathBuf},
15 time::{Duration, Instant},
16};
17use thiserror::Error as ThisError;
18
19mod build_network;
20mod build_snapshot;
21mod build_targets;
22mod capabilities;
23mod clock;
24mod commands;
25mod config_selection;
26mod coordinator_install;
27mod coordinator_install_journal;
28mod current_execution;
29mod deployment_truth_gate;
30mod execution_preflight;
31mod fleet_install_session;
32mod fleet_registry_activation;
33mod fleet_registry_activation_journal;
34mod fleet_subnet_root_component_registry_preparation;
35mod fleet_subnet_root_install;
36mod fleet_subnet_root_install_journal;
37mod fleet_subnet_root_registry_join;
38mod fleet_subnet_root_registry_mirror_activation;
39mod fleet_subnet_root_registry_sync;
40mod fleet_subnet_root_store_bootstrap;
41mod identity;
42mod operations;
43mod options;
44mod output;
45mod phase_receipts;
46mod plan_artifacts;
47mod preparation;
48mod receipt_io;
49mod timing;
50mod truth_check;
51
52use crate::release_build::{ReleaseBuildPlanError, plan_release_build};
53use build_network::resolve_install_build_context;
54use build_snapshot::resolve_install_snapshot;
55pub use config_selection::{
56 ConfigDiscoveryError, current_canic_project_root, discover_canic_config_choices,
57 discover_canic_project_root_from, discover_project_canic_config_choices, project_app_roots,
58 select_discovered_app_config_path,
59};
60use coordinator_install::install_and_verify_fleet_coordinator;
61use current_execution::current_install_execution_context;
62use fleet_registry_activation::{ActivateFleetRegistryRequest, activate_and_verify_fleet_registry};
63use fleet_subnet_root_component_registry_preparation::{
64 PrepareFleetSubnetRootComponentRegistriesRequest,
65 prepare_and_verify_fleet_subnet_root_component_registries,
66};
67use fleet_subnet_root_install::install_and_verify_fleet_subnet_roots;
68use fleet_subnet_root_registry_join::register_and_verify_fleet_subnet_roots_joining;
69use fleet_subnet_root_registry_mirror_activation::{
70 ActivateFleetSubnetRootRegistryMirrorsRequest,
71 activate_and_verify_fleet_subnet_root_registry_mirrors,
72};
73use fleet_subnet_root_registry_sync::{
74 SynchronizeFleetSubnetRootsRequest, synchronize_and_verify_fleet_subnet_roots,
75};
76use fleet_subnet_root_store_bootstrap::bootstrap_and_verify_fleet_subnet_root_stores;
77use identity::resolve_install_identity;
78pub use options::InstallRootOptions;
79use output::print_install_timing_summary;
80use phase_receipts::{
81 CompletedInstallPhase, InstallReceiptScope, write_completed_install_phase_receipt,
82};
83use plan_artifacts::emit_manifest_with_phase;
84use preparation::prepare_install_deployment_truth;
85pub use receipt_io::latest_deployment_truth_receipt_path_from_root;
86use timing::InstallTimingSummary as CurrentInstallTimingSummary;
87pub use truth_check::{check_install_deployment_truth, check_install_execution_preflight};
88
89#[cfg(test)]
90mod tests;
91
92#[derive(Clone, Copy, Debug, Eq, PartialEq)]
99pub enum InstallRootBlockKind {
100 DeploymentExecutionPreflight,
101 DeploymentTruth,
102}
103
104#[derive(Debug, ThisError)]
111#[error("{message}")]
112pub struct InstallRootBlockedError {
113 kind: InstallRootBlockKind,
114 message: String,
115}
116
117impl InstallRootBlockedError {
118 pub(super) const fn new(kind: InstallRootBlockKind, message: String) -> Self {
119 Self { kind, message }
120 }
121
122 #[must_use]
123 pub const fn kind(&self) -> InstallRootBlockKind {
124 self.kind
125 }
126}
127
128#[derive(Clone, Copy, Debug, Eq, PartialEq)]
130pub enum InstallRootPhase {
131 WorkspaceDiscovery,
132 ProjectDiscovery,
133 Configuration,
134 BuildInputs,
135 Identity,
136 Preparation,
137 Manifest,
138 Planning,
139 Activation,
140}
141
142impl fmt::Display for InstallRootPhase {
143 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
144 formatter.write_str(match self {
145 Self::WorkspaceDiscovery => "workspace discovery",
146 Self::ProjectDiscovery => "ICP project discovery",
147 Self::Configuration => "configuration selection",
148 Self::BuildInputs => "build input validation",
149 Self::Identity => "deployment identity resolution",
150 Self::Preparation => "deployment preparation",
151 Self::Manifest => "manifest emission",
152 Self::Planning => "Fleet installation planning",
153 Self::Activation => "root activation",
154 })
155 }
156}
157
158#[derive(Debug, ThisError)]
160#[error("root install failed during {phase}: {source}")]
161pub struct InstallRootError {
162 phase: InstallRootPhase,
163 #[source]
164 source: Box<dyn std::error::Error>,
165}
166
167impl InstallRootError {
168 pub fn new<E>(phase: InstallRootPhase, source: E) -> Self
170 where
171 E: std::error::Error + 'static,
172 {
173 Self {
174 phase,
175 source: Box::new(source),
176 }
177 }
178
179 fn from_boxed(phase: InstallRootPhase, source: Box<dyn std::error::Error>) -> Self {
180 Self { phase, source }
181 }
182
183 fn in_phase(phase: InstallRootPhase) -> impl FnOnce(Box<dyn std::error::Error>) -> Self {
184 move |source| Self::from_boxed(phase, source)
185 }
186
187 #[must_use]
188 pub const fn phase(&self) -> InstallRootPhase {
189 self.phase
190 }
191}
192
193#[derive(Debug, ThisError)]
194#[error(
195 "Fleet Coordinator {coordinator} and {active_roots} planned Fleet Subnet Root(s) now have independently verified all-Active Registry mirrors, matching Fleet Directories and empty Component Registries at revision {active_registry_revision} from the durable plan at {}; Component allocation and runtime activation remain blocked until their Registry-bound lifecycle is implemented",
196 plan_path.display(),
197)]
198struct ComponentRuntimeActivationUnavailableError {
199 plan_path: PathBuf,
200 coordinator: canic_core::cdk::types::Principal,
201 active_roots: usize,
202 active_registry_revision: u64,
203}
204
205#[derive(Debug, ThisError)]
206#[error("fresh Fleet installation requires --fleet-input <PATH>")]
207struct MissingFleetInstallInputError;
208
209pub fn discover_current_canic_config_choices() -> Result<Vec<PathBuf>, ConfigDiscoveryError> {
211 let project_root = current_canic_project_root()?;
212 let choices = config_selection::discover_workspace_canic_config_choices(&project_root)?;
213 if !choices.is_empty() {
214 return Ok(choices);
215 }
216
217 let icp_root = icp_root()?;
218 if icp_root != project_root {
219 return config_selection::discover_workspace_canic_config_choices(&icp_root);
220 }
221
222 Ok(choices)
223}
224
225pub fn install_root(options: InstallRootOptions) -> Result<(), InstallRootError> {
227 let (workspace_root, icp_root) = resolve_current_install_roots(&options)?;
228 let _build_cache_cleanup = DefaultCanisterBuildCacheCleanup::for_install(&workspace_root);
229 let config_path = current_install_config_path(&icp_root, &options)?;
230 let (build_context, install_snapshot) =
231 current_install_build_inputs(&workspace_root, &icp_root, &config_path, &options)
232 .map_err(InstallRootError::in_phase(InstallRootPhase::BuildInputs))?;
233 let (app_id, fleet_name) =
234 resolve_install_identity(&options, &config_path, &install_snapshot.app_id)
235 .map_err(InstallRootError::in_phase(InstallRootPhase::Identity))?;
236 let total_started_at = Instant::now();
237 let mut timings = CurrentInstallTimingSummary::default();
238 let environment = options.environment.as_str();
239 let execution_context = current_install_execution_context(
240 &workspace_root,
241 &icp_root,
242 options.artifact_environment(),
243 );
244 let resolved_fleet_install_input =
245 resolve_current_fleet_install_input(&icp_root, environment, &options)
246 .map_err(InstallRootError::in_phase(InstallRootPhase::Planning))?;
247
248 print_install_identity(&app_id, &fleet_name);
249 let prepared = prepare_install_deployment_truth(
250 &options,
251 &icp_root,
252 &config_path,
253 &fleet_name,
254 &execution_context,
255 &build_context,
256 &install_snapshot,
257 )
258 .map_err(InstallRootError::in_phase(InstallRootPhase::Preparation))?;
259 timings.build_all = prepared.timings.build_all;
260 let emitted_manifest = emit_manifest_with_phase(
261 &icp_root,
262 &install_snapshot,
263 &prepared.build_outputs,
264 &prepared.infrastructure_build_outputs,
265 prepared.plan_artifacts.as_ref(),
266 )
267 .map_err(InstallRootError::in_phase(InstallRootPhase::Manifest))?;
268 timings.emit_manifest = emitted_manifest.duration;
269 let finalized_release_build =
270 require_finalized_release_build(emitted_manifest.finalized_release_build)?;
271 let planned_install = plan_current_fleet_install(
272 &icp_root,
273 environment,
274 &fleet_name,
275 &app_id,
276 &config_path,
277 &finalized_release_build,
278 resolved_fleet_install_input,
279 )?;
280 let receipt_scope = InstallReceiptScope {
281 icp_root: &icp_root,
282 fleet: planned_install.fleet(),
283 check: &prepared.deployment_truth_check,
284 execution_context: Some(&execution_context),
285 };
286 persist_current_pre_root_receipts(
287 receipt_scope,
288 &prepared.pre_activation_receipts,
289 prepared.build_phase,
290 emitted_manifest.phase,
291 )?;
292 install_current_fleet_infrastructure(
293 &icp_root,
294 environment,
295 build_context.local_replica.as_ref(),
296 &config_path,
297 &planned_install,
298 &mut timings,
299 )?;
300
301 print_install_timing_summary(&timings, total_started_at.elapsed());
302 Ok(())
303}
304
305fn install_current_fleet_infrastructure(
306 icp_root: &Path,
307 environment: &str,
308 local_replica: Option<&crate::icp::LocalReplicaTarget>,
309 config_path: &Path,
310 planned: &PlannedCurrentFleetInstall,
311 timings: &mut CurrentInstallTimingSummary,
312) -> Result<(), InstallRootError> {
313 let (coordinator, coordinator_duration) = install_current_fleet_coordinator(
314 icp_root,
315 environment,
316 local_replica,
317 config_path,
318 &planned.plan,
319 )?;
320 timings.create_canisters = coordinator_duration;
321 let (roots, roots_duration) = install_current_fleet_subnet_roots(
322 icp_root,
323 environment,
324 local_replica,
325 config_path,
326 planned,
327 coordinator.coordinator,
328 )?;
329 timings.create_canisters += roots_duration;
330 bootstrap_and_verify_fleet_subnet_root_stores(
331 icp_root,
332 environment,
333 local_replica,
334 config_path,
335 &planned.plan,
336 coordinator.coordinator,
337 planned.session.operation_id,
338 )
339 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
340 let joining_version = register_and_verify_fleet_subnet_roots_joining(
341 icp_root,
342 environment,
343 local_replica,
344 config_path,
345 &planned.plan,
346 coordinator.coordinator,
347 planned.session.operation_id,
348 )
349 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
350 synchronize_and_verify_fleet_subnet_roots(SynchronizeFleetSubnetRootsRequest {
351 icp_root,
352 environment,
353 local_replica,
354 config_path,
355 fleet_install_plan: &planned.plan,
356 coordinator: coordinator.coordinator,
357 install_operation_id: planned.session.operation_id,
358 joining_version: joining_version.clone(),
359 })
360 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
361 let active = activate_and_verify_fleet_registry(ActivateFleetRegistryRequest {
362 icp_root,
363 environment,
364 local_replica,
365 config_path,
366 fleet_install_plan: &planned.plan,
367 coordinator: coordinator.coordinator,
368 install_operation_id: planned.session.operation_id,
369 joining_version: joining_version.clone(),
370 })
371 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
372 activate_and_verify_fleet_subnet_root_registry_mirrors(
373 ActivateFleetSubnetRootRegistryMirrorsRequest {
374 icp_root,
375 environment,
376 local_replica,
377 config_path,
378 fleet_install_plan: &planned.plan,
379 coordinator: coordinator.coordinator,
380 install_operation_id: planned.session.operation_id,
381 joining_version,
382 active_registry: &active.registry,
383 active_version: active.version.clone(),
384 },
385 )
386 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
387 prepare_and_verify_fleet_subnet_root_component_registries(
388 PrepareFleetSubnetRootComponentRegistriesRequest {
389 icp_root,
390 environment,
391 local_replica,
392 config_path,
393 fleet_install_plan: &planned.plan,
394 coordinator: coordinator.coordinator,
395 install_operation_id: planned.session.operation_id,
396 },
397 )
398 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
399 require_component_runtime_activation(
400 &planned.plan.path,
401 coordinator.coordinator,
402 roots.roots.len(),
403 active.version.revision,
404 )
405 .map_err(|source| InstallRootError::new(InstallRootPhase::Activation, source))
406}
407
408fn resolve_current_install_roots(
409 options: &InstallRootOptions,
410) -> Result<(PathBuf, PathBuf), InstallRootError> {
411 let workspace_root = workspace_root()
412 .map_err(|source| InstallRootError::new(InstallRootPhase::WorkspaceDiscovery, source))?;
413 let icp_root = match &options.icp_root {
414 Some(path) => path
415 .canonicalize()
416 .map_err(|source| InstallRootError::new(InstallRootPhase::ProjectDiscovery, source))?,
417 None => icp_root()
418 .map_err(|source| InstallRootError::new(InstallRootPhase::ProjectDiscovery, source))?,
419 };
420 Ok((workspace_root, icp_root))
421}
422
423fn plan_current_fleet_install(
424 icp_root: &Path,
425 environment: &str,
426 fleet_name: &str,
427 app_id: &str,
428 config_path: &Path,
429 finalized_release_build: &crate::release_build::FinalizedReleaseBuild,
430 input: ResolvedFleetInstallInput,
431) -> Result<PlannedCurrentFleetInstall, InstallRootError> {
432 let session = plan_current_fleet_install_session(
433 icp_root,
434 environment,
435 fleet_name,
436 app_id,
437 finalized_release_build,
438 )?;
439 let plan = persist_current_fleet_install_plan(
440 icp_root,
441 config_path,
442 session.fleet.clone(),
443 finalized_release_build,
444 input,
445 )
446 .map_err(InstallRootError::in_phase(InstallRootPhase::Planning))?;
447 Ok(PlannedCurrentFleetInstall { session, plan })
448}
449
450struct PlannedCurrentFleetInstall {
451 session: fleet_install_session::FleetInstallSession,
452 plan: PersistedFleetInstallPlan,
453}
454
455impl PlannedCurrentFleetInstall {
456 const fn fleet(&self) -> canic_core::ids::FleetKey {
457 self.session.fleet.fleet
458 }
459}
460
461fn require_finalized_release_build(
462 finalized: Option<crate::release_build::FinalizedReleaseBuild>,
463) -> Result<crate::release_build::FinalizedReleaseBuild, InstallRootError> {
464 finalized.ok_or_else(|| {
465 InstallRootError::new(
466 InstallRootPhase::Manifest,
467 ReleaseBuildPlanError::MissingFinalizedAuthority,
468 )
469 })
470}
471
472fn resolve_current_fleet_install_input(
473 icp_root: &Path,
474 environment: &str,
475 options: &InstallRootOptions,
476) -> Result<ResolvedFleetInstallInput, Box<dyn std::error::Error>> {
477 let input_path = options
478 .fleet_install_input_path
479 .as_ref()
480 .ok_or(MissingFleetInstallInputError)?;
481 let input_path = if input_path.is_absolute() {
482 input_path.clone()
483 } else {
484 icp_root.join(input_path)
485 };
486 load_and_resolve_fleet_install_input(icp_root, environment, &input_path).map_err(Into::into)
487}
488
489fn persist_current_fleet_install_plan(
490 icp_root: &Path,
491 config_path: &Path,
492 fleet: canic_core::ids::FleetBinding,
493 finalized_release_build: &crate::release_build::FinalizedReleaseBuild,
494 input: ResolvedFleetInstallInput,
495) -> Result<PersistedFleetInstallPlan, Box<dyn std::error::Error>> {
496 let config = AppConfigSnapshot::load(config_path)?;
497 compile_and_persist_fleet_install_plan(FleetInstallPlanRequest {
498 root: icp_root,
499 config: config.model(),
500 fleet,
501 release_build_id: finalized_release_build.record.release_build_id,
502 coordinator: input.coordinator,
503 fleet_subnet_roots: input.fleet_subnet_roots,
504 })
505 .map_err(Into::into)
506}
507
508fn require_component_runtime_activation(
509 plan_path: &Path,
510 coordinator: canic_core::cdk::types::Principal,
511 active_roots: usize,
512 active_registry_revision: u64,
513) -> Result<(), ComponentRuntimeActivationUnavailableError> {
514 Err(ComponentRuntimeActivationUnavailableError {
515 plan_path: plan_path.to_path_buf(),
516 coordinator,
517 active_roots,
518 active_registry_revision,
519 })
520}
521
522fn current_install_config_path(
523 icp_root: &Path,
524 options: &InstallRootOptions,
525) -> Result<PathBuf, InstallRootError> {
526 resolve_install_config_path(
527 icp_root,
528 options.config_path.as_deref(),
529 options.interactive_config_selection,
530 )
531 .map_err(InstallRootError::in_phase(InstallRootPhase::Configuration))
532}
533
534fn plan_current_fleet_install_session(
535 icp_root: &Path,
536 environment: &str,
537 fleet_name: &str,
538 app_id: &str,
539 finalized_release_build: &crate::release_build::FinalizedReleaseBuild,
540) -> Result<fleet_install_session::FleetInstallSession, InstallRootError> {
541 let canonical_network_id = resolve_canonical_network_id_from_root(icp_root, environment)
542 .map_err(|source| InstallRootError::new(InstallRootPhase::Activation, source))?;
543 let fleet_name = fleet_name
544 .parse()
545 .map_err(|source| InstallRootError::new(InstallRootPhase::Identity, source))?;
546 fleet_install_session::plan_fleet_install_session(
547 fleet_install_session::PlanFleetInstallSessionRequest {
548 root: icp_root,
549 canonical_network_id,
550 fleet_name,
551 app: app_id.into(),
552 finalized_release_build,
553 },
554 )
555 .map_err(|source| InstallRootError::new(InstallRootPhase::Activation, source))
556}
557
558fn print_install_identity(app: &str, fleet_name: &str) {
559 println!("Installing Fleet {fleet_name}");
560 println!("Source App {app}");
561 println!();
562}
563
564fn persist_current_pre_root_receipts(
565 receipt_scope: InstallReceiptScope<'_>,
566 prepared_receipts: &[DeploymentReceiptV1],
567 build_phase: CompletedInstallPhase,
568 manifest_phase: CompletedInstallPhase,
569) -> Result<(), InstallRootError> {
570 persist_pre_root_receipts(
571 receipt_scope,
572 prepared_receipts,
573 build_phase,
574 manifest_phase,
575 )
576 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))
577}
578
579fn install_current_fleet_coordinator(
580 icp_root: &Path,
581 environment: &str,
582 local_replica: Option<&crate::icp::LocalReplicaTarget>,
583 config_path: &Path,
584 plan: &PersistedFleetInstallPlan,
585) -> Result<(coordinator_install::VerifiedFleetCoordinator, Duration), InstallRootError> {
586 let started = Instant::now();
587 let coordinator = install_and_verify_fleet_coordinator(
588 icp_root,
589 environment,
590 local_replica,
591 config_path,
592 plan,
593 )
594 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
595 Ok((coordinator, started.elapsed()))
596}
597
598fn install_current_fleet_subnet_roots(
599 icp_root: &Path,
600 environment: &str,
601 local_replica: Option<&crate::icp::LocalReplicaTarget>,
602 config_path: &Path,
603 planned: &PlannedCurrentFleetInstall,
604 coordinator: canic_core::cdk::types::Principal,
605) -> Result<
606 (
607 fleet_subnet_root_install::VerifiedFleetSubnetRoots,
608 Duration,
609 ),
610 InstallRootError,
611> {
612 let started = Instant::now();
613 let roots = install_and_verify_fleet_subnet_roots(
614 icp_root,
615 environment,
616 local_replica,
617 config_path,
618 &planned.plan,
619 coordinator,
620 planned.session.operation_id,
621 )
622 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
623 Ok((roots, started.elapsed()))
624}
625
626fn persist_pre_root_receipts(
627 receipt_scope: InstallReceiptScope<'_>,
628 prepared_receipts: &[DeploymentReceiptV1],
629 build_phase: CompletedInstallPhase,
630 manifest_phase: CompletedInstallPhase,
631) -> Result<(), Box<dyn std::error::Error>> {
632 for receipt in prepared_receipts {
633 receipt_scope.write_receipt(receipt)?;
634 }
635 write_completed_install_phase_receipt(receipt_scope, build_phase)?;
636 write_completed_install_phase_receipt(receipt_scope, manifest_phase)?;
637 Ok(())
638}
639
640fn current_install_build_inputs(
641 workspace_root: &std::path::Path,
642 icp_root: &std::path::Path,
643 config_path: &std::path::Path,
644 options: &InstallRootOptions,
645) -> Result<
646 (
647 crate::canister_build::WorkspaceBuildContext,
648 build_snapshot::ValidatedInstallSnapshot,
649 ),
650 Box<dyn std::error::Error>,
651> {
652 let mut context = resolve_install_build_context(
653 workspace_root,
654 icp_root,
655 config_path,
656 &options.environment,
657 &options.root_build_target,
658 options.build_profile,
659 )?;
660 let mut snapshot = resolve_install_snapshot(
661 &context,
662 &options.root_build_target,
663 options.deployment_plan_override.is_some(),
664 )?;
665 if snapshot.complete_build.is_some() {
666 let release_build = plan_release_build(icp_root)?;
667 context = context.with_release_build_id(release_build.record.release_build_id);
668 snapshot.release_build = Some(release_build);
669 }
670 Ok((context, snapshot))
671}