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
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///
112/// InstallRootBlockKind
113///
114/// Machine-readable reason that a fresh root install stopped before mutation.
115///
116
117#[derive(Clone, Copy, Debug, Eq, PartialEq)]
118pub enum InstallRootBlockKind {
119    DeploymentExecutionPreflight,
120    DeploymentTruth,
121}
122
123///
124/// InstallRootBlockedError
125///
126/// Typed install block retained through the host/CLI error boundary.
127///
128
129#[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/// Stable phase in which a root install failed.
148#[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/// Typed public failure for the root-install workflow.
178#[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    /// Preserve a concrete cause while assigning it to a stable install phase.
188    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
212/// Discover installable Canic config choices under the current workspace.
213pub 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
228// Execute fresh Fleet planning and the Coordinator-first installation workflow.
229pub 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}