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