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_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///
102/// InstallRootBlockKind
103///
104/// Machine-readable reason that a fresh root install stopped before mutation.
105///
106
107#[derive(Clone, Copy, Debug, Eq, PartialEq)]
108pub enum InstallRootBlockKind {
109    DeploymentExecutionPreflight,
110    DeploymentTruth,
111}
112
113///
114/// InstallRootBlockedError
115///
116/// Typed install block retained through the host/CLI error boundary.
117///
118
119#[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/// Stable phase in which a root install failed.
138#[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/// Typed public failure for the root-install workflow.
168#[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    /// Preserve a concrete cause while assigning it to a stable install phase.
178    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
206/// Discover installable Canic config choices under the current workspace.
207pub 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
222// Execute fresh Fleet planning and the Coordinator-first installation workflow.
223pub 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}