Skip to main content

canic_host/install_root/
mod.rs

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///
93/// InstallRootBlockKind
94///
95/// Machine-readable reason that a fresh root install stopped before mutation.
96///
97
98#[derive(Clone, Copy, Debug, Eq, PartialEq)]
99pub enum InstallRootBlockKind {
100    DeploymentExecutionPreflight,
101    DeploymentTruth,
102}
103
104///
105/// InstallRootBlockedError
106///
107/// Typed install block retained through the host/CLI error boundary.
108///
109
110#[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/// Stable phase in which a root install failed.
129#[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/// Typed public failure for the root-install workflow.
159#[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    /// Preserve a concrete cause while assigning it to a stable install phase.
169    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
209/// Discover installable Canic config choices under the current workspace.
210pub 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
225// Execute fresh Fleet planning and the Coordinator-first installation workflow.
226pub 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}