Skip to main content

canic_host/install_root/
mod.rs

1use crate::{
2    canister_build::cache::DefaultCanisterBuildCacheCleanup,
3    network::resolve_canonical_network_id_from_root,
4    release_set::{icp_root, workspace_root},
5};
6use config_selection::resolve_install_config_path;
7use std::{
8    fmt,
9    path::{Path, PathBuf},
10    time::Instant,
11};
12use thiserror::Error as ThisError;
13
14mod activation;
15mod build_network;
16mod build_snapshot;
17mod build_targets;
18mod capabilities;
19mod clock;
20mod commands;
21mod config_selection;
22mod current_execution;
23mod deployment_registration;
24mod deployment_truth_gate;
25mod execution_preflight;
26mod fleet_activation_journal;
27mod identity;
28mod operations;
29mod options;
30mod output;
31mod phase_receipts;
32mod plan_artifacts;
33mod preparation;
34mod receipt_io;
35mod root_canister;
36mod root_cycles;
37mod root_verification;
38mod state;
39mod timing;
40mod truth_check;
41
42use crate::release_build::{ReleaseBuildPlanError, plan_release_build};
43use activation::{PreparedRootInstall, install_root_prepared};
44use build_network::resolve_install_build_context;
45use build_snapshot::resolve_install_snapshot;
46pub use config_selection::{
47    ConfigDiscoveryError, current_canic_project_root, discover_canic_config_choices,
48    discover_canic_project_root_from, discover_project_canic_config_choices, project_app_roots,
49    select_discovered_app_config_path,
50};
51use current_execution::current_install_execution_context;
52pub use deployment_registration::{
53    RegisterDeploymentStateOptions, VerifyDeploymentRootOptions, register_deployment_state,
54    verify_registered_deployment_root,
55};
56use identity::resolve_install_identity;
57pub use operations::{
58    InstallRootActivationStatusError, InstallRootExecutionReconciliationError,
59    InstallRootModuleVerificationError,
60};
61pub use options::InstallRootOptions;
62use output::print_install_timing_summary;
63pub use phase_receipts::InstallPhaseFailureError;
64use phase_receipts::InstallReceiptScope;
65use plan_artifacts::emit_manifest_with_deployment_truth_receipt;
66use preparation::{prepare_install_deployment_truth, resolve_root_canister_after_manifest};
67pub use receipt_io::latest_deployment_truth_receipt_path_from_root;
68pub(crate) use state::validate_environment_name;
69pub use state::{
70    InstallState, InstallStateError, RootVerificationStatus,
71    read_named_deployment_install_state_from_root,
72};
73use timing::InstallTimingSummary as CurrentInstallTimingSummary;
74pub use truth_check::{check_install_deployment_truth, check_install_execution_preflight};
75
76#[cfg(test)]
77mod tests;
78
79///
80/// InstallRootBlockKind
81///
82/// Machine-readable reason that a fresh root install stopped before mutation.
83///
84
85#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub enum InstallRootBlockKind {
87    DeploymentExecutionPreflight,
88    DeploymentTruth,
89}
90
91///
92/// InstallRootBlockedError
93///
94/// Typed install block retained through the host/CLI error boundary.
95///
96
97#[derive(Debug, ThisError)]
98#[error("{message}")]
99pub struct InstallRootBlockedError {
100    kind: InstallRootBlockKind,
101    message: String,
102}
103
104impl InstallRootBlockedError {
105    pub(super) const fn new(kind: InstallRootBlockKind, message: String) -> Self {
106        Self { kind, message }
107    }
108
109    #[must_use]
110    pub const fn kind(&self) -> InstallRootBlockKind {
111        self.kind
112    }
113}
114
115/// Stable phase in which a root install failed.
116#[derive(Clone, Copy, Debug, Eq, PartialEq)]
117pub enum InstallRootPhase {
118    WorkspaceDiscovery,
119    ProjectDiscovery,
120    Configuration,
121    BuildInputs,
122    Identity,
123    Preparation,
124    Manifest,
125    Activation,
126}
127
128impl fmt::Display for InstallRootPhase {
129    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
130        formatter.write_str(match self {
131            Self::WorkspaceDiscovery => "workspace discovery",
132            Self::ProjectDiscovery => "ICP project discovery",
133            Self::Configuration => "configuration selection",
134            Self::BuildInputs => "build input validation",
135            Self::Identity => "deployment identity resolution",
136            Self::Preparation => "deployment preparation",
137            Self::Manifest => "manifest emission",
138            Self::Activation => "root activation",
139        })
140    }
141}
142
143/// Typed public failure for the root-install workflow.
144#[derive(Debug, ThisError)]
145#[error("root install failed during {phase}: {source}")]
146pub struct InstallRootError {
147    phase: InstallRootPhase,
148    #[source]
149    source: Box<dyn std::error::Error>,
150}
151
152/// Typed terminal outcome while the next activation phase is not yet admitted.
153#[derive(Debug, ThisError)]
154#[error(
155    "root {root_canister_id} is durably Prepared at activation journal {} sequence {sequence}; no operational Fleet state was published",
156    journal_path.display()
157)]
158pub struct FleetActivationContinuationRequired {
159    root_canister_id: String,
160    journal_path: PathBuf,
161    sequence: u64,
162}
163
164impl FleetActivationContinuationRequired {
165    #[must_use]
166    pub fn root_canister_id(&self) -> &str {
167        &self.root_canister_id
168    }
169
170    #[must_use]
171    pub fn journal_path(&self) -> &Path {
172        &self.journal_path
173    }
174
175    #[must_use]
176    pub const fn sequence(&self) -> u64 {
177        self.sequence
178    }
179}
180
181fn continuation_required(
182    root_canister_id: String,
183    prepared_root: &PreparedRootInstall,
184) -> InstallRootError {
185    InstallRootError::new(
186        InstallRootPhase::Activation,
187        FleetActivationContinuationRequired {
188            root_canister_id,
189            journal_path: prepared_root.activation.path.clone(),
190            sequence: prepared_root.activation.journal.sequence,
191        },
192    )
193}
194
195impl InstallRootError {
196    /// Preserve a concrete cause while assigning it to a stable install phase.
197    pub fn new<E>(phase: InstallRootPhase, source: E) -> Self
198    where
199        E: std::error::Error + 'static,
200    {
201        Self {
202            phase,
203            source: Box::new(source),
204        }
205    }
206
207    fn from_boxed(phase: InstallRootPhase, source: Box<dyn std::error::Error>) -> Self {
208        Self { phase, source }
209    }
210
211    fn in_phase(phase: InstallRootPhase) -> impl FnOnce(Box<dyn std::error::Error>) -> Self {
212        move |source| Self::from_boxed(phase, source)
213    }
214
215    #[must_use]
216    pub const fn phase(&self) -> InstallRootPhase {
217        self.phase
218    }
219}
220
221/// Discover installable Canic config choices under the current workspace.
222pub fn discover_current_canic_config_choices() -> Result<Vec<PathBuf>, ConfigDiscoveryError> {
223    let project_root = current_canic_project_root()?;
224    let choices = config_selection::discover_workspace_canic_config_choices(&project_root)?;
225    if !choices.is_empty() {
226        return Ok(choices);
227    }
228
229    let icp_root = icp_root()?;
230    if icp_root != project_root {
231        return config_selection::discover_workspace_canic_config_choices(&icp_root);
232    }
233
234    Ok(choices)
235}
236
237// Execute the local thin-root install flow against an already running replica.
238pub fn install_root(options: InstallRootOptions) -> Result<(), InstallRootError> {
239    let workspace_root = workspace_root()
240        .map_err(|source| InstallRootError::new(InstallRootPhase::WorkspaceDiscovery, source))?;
241    let _build_cache_cleanup = DefaultCanisterBuildCacheCleanup::for_install(&workspace_root);
242    let icp_root = match &options.icp_root {
243        Some(path) => path
244            .canonicalize()
245            .map_err(|source| InstallRootError::new(InstallRootPhase::ProjectDiscovery, source))?,
246        None => icp_root()
247            .map_err(|source| InstallRootError::new(InstallRootPhase::ProjectDiscovery, source))?,
248    };
249    let config_path = resolve_install_config_path(
250        &icp_root,
251        options.config_path.as_deref(),
252        options.interactive_config_selection,
253    )
254    .map_err(InstallRootError::in_phase(InstallRootPhase::Configuration))?;
255    let (build_context, install_snapshot) =
256        current_install_build_inputs(&workspace_root, &icp_root, &config_path, &options)
257            .map_err(InstallRootError::in_phase(InstallRootPhase::BuildInputs))?;
258    let (app_id, fleet_name) =
259        resolve_install_identity(&options, &config_path, &install_snapshot.app_id)
260            .map_err(InstallRootError::in_phase(InstallRootPhase::Identity))?;
261    let total_started_at = Instant::now();
262    let mut timings = CurrentInstallTimingSummary::default();
263    let environment = options.environment.as_str();
264    let execution_context = current_install_execution_context(
265        &workspace_root,
266        &icp_root,
267        options.artifact_environment(),
268    );
269
270    println!("Installing Fleet {fleet_name}");
271    println!("Source App {app_id}");
272    println!();
273    let prepared = prepare_install_deployment_truth(
274        &options,
275        &icp_root,
276        &config_path,
277        &fleet_name,
278        &execution_context,
279        &build_context,
280        &install_snapshot,
281    )
282    .map_err(InstallRootError::in_phase(InstallRootPhase::Preparation))?;
283    timings.build_all = prepared.timings.build_all;
284    let receipt_scope = InstallReceiptScope {
285        icp_root: &icp_root,
286        environment,
287        deployment_name: &fleet_name,
288        check: &prepared.deployment_truth_check,
289        execution_context: Some(&execution_context),
290    };
291
292    let (_manifest_path, emit_manifest_duration, finalized_release_build) =
293        emit_manifest_with_deployment_truth_receipt(
294            receipt_scope,
295            &options,
296            &install_snapshot,
297            &prepared.build_outputs,
298            prepared.plan_artifacts.as_ref(),
299        )
300        .map_err(InstallRootError::in_phase(InstallRootPhase::Manifest))?;
301    timings.emit_manifest = emit_manifest_duration;
302    let finalized_release_build = finalized_release_build.ok_or_else(|| {
303        InstallRootError::new(
304            InstallRootPhase::Manifest,
305            ReleaseBuildPlanError::MissingFinalizedAuthority,
306        )
307    })?;
308    let canonical_network_id = resolve_canonical_network_id_from_root(&icp_root, environment)
309        .map_err(|source| InstallRootError::new(InstallRootPhase::Activation, source))?;
310    let activation = fleet_activation_journal::plan_fleet_install_activation(
311        fleet_activation_journal::PlanFleetInstallActivationRequest {
312            root: &icp_root,
313            canonical_network_id,
314            fleet_name: fleet_name
315                .parse()
316                .map_err(|source| InstallRootError::new(InstallRootPhase::Identity, source))?,
317            app: app_id.into(),
318            finalized_release_build: &finalized_release_build,
319        },
320    )
321    .map_err(|source| InstallRootError::new(InstallRootPhase::Activation, source))?;
322    let (root_canister_id, create_duration) =
323        resolve_root_canister_after_manifest(receipt_scope, &options, &config_path, &build_context)
324            .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
325    timings.create_canisters = create_duration;
326    let prepared_root = install_root_prepared(
327        receipt_scope,
328        &options,
329        &root_canister_id,
330        &build_context,
331        prepared.plan_artifacts.as_ref(),
332        &activation,
333    )
334    .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
335    timings.record_activation(prepared_root.timings);
336
337    print_install_timing_summary(&timings, total_started_at.elapsed());
338    Err(continuation_required(root_canister_id, &prepared_root))
339}
340
341fn current_install_build_inputs(
342    workspace_root: &std::path::Path,
343    icp_root: &std::path::Path,
344    config_path: &std::path::Path,
345    options: &InstallRootOptions,
346) -> Result<
347    (
348        crate::canister_build::WorkspaceBuildContext,
349        build_snapshot::ValidatedInstallSnapshot,
350    ),
351    Box<dyn std::error::Error>,
352> {
353    let mut context = resolve_install_build_context(
354        workspace_root,
355        icp_root,
356        config_path,
357        &options.environment,
358        &options.root_build_target,
359        options.build_profile,
360    )?;
361    let mut snapshot = resolve_install_snapshot(
362        &context,
363        &options.root_build_target,
364        options.deployment_plan_override.is_some(),
365    )?;
366    if snapshot.complete_build.is_some() {
367        let release_build = plan_release_build(icp_root)?;
368        context = context.with_release_build_id(release_build.record.release_build_id);
369        snapshot.release_build = Some(release_build);
370    }
371    Ok((context, snapshot))
372}