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