Skip to main content

canic_host/install_root/
mod.rs

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