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_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 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_network::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_environment_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 environment = options.environment.as_str();
230    let execution_context = current_install_execution_context(
231        &workspace_root,
232        &icp_root,
233        options.artifact_environment(),
234    );
235
236    println!("Installing deployment {deployment_name}");
237    println!("Fleet template {fleet_name}");
238    println!();
239    let prepared = prepare_install_deployment_truth(
240        &options,
241        &icp_root,
242        &config_path,
243        &deployment_name,
244        &execution_context,
245        &build_context,
246        &install_snapshot,
247    )
248    .map_err(InstallRootError::in_phase(InstallRootPhase::Preparation))?;
249    timings.create_canisters = prepared.timings.create_canisters;
250    timings.build_all = prepared.timings.build_all;
251
252    let (manifest_path, emit_manifest_duration) = emit_manifest_with_deployment_truth_receipt(
253        &icp_root,
254        &options,
255        &deployment_name,
256        &prepared.deployment_truth_check,
257        &execution_context,
258        &install_snapshot,
259        &prepared.build_outputs,
260        prepared.plan_artifacts.as_ref(),
261    )
262    .map_err(InstallRootError::in_phase(InstallRootPhase::Manifest))?;
263    timings.emit_manifest = emit_manifest_duration;
264    let activation_timings = run_root_activation_phases(
265        InstallReceiptScope {
266            icp_root: &icp_root,
267            environment,
268            deployment_name: &deployment_name,
269            check: &prepared.deployment_truth_check,
270            execution_context: Some(&execution_context),
271        },
272        &options,
273        &prepared.root_canister_id,
274        &manifest_path,
275        total_started_at,
276        &build_context,
277        prepared.plan_artifacts.as_ref(),
278    )
279    .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
280    timings.install_root = activation_timings.install_root;
281    timings.fund_root = activation_timings.fund_root;
282    timings.stage_release_set = activation_timings.stage_release_set;
283    timings.resume_bootstrap = activation_timings.resume_bootstrap;
284    timings.wait_ready = activation_timings.wait_ready;
285    timings.finalize_root_funding = activation_timings.finalize_root_funding;
286
287    print_install_timing_summary(&timings, total_started_at.elapsed());
288    persist_install_result(
289        InstallReceiptScope {
290            icp_root: &icp_root,
291            environment,
292            deployment_name: &deployment_name,
293            check: &prepared.deployment_truth_check,
294            execution_context: Some(&execution_context),
295        },
296        &options,
297        &workspace_root,
298        &config_path,
299        &manifest_path,
300        InstallCompletion {
301            fleet_name: &fleet_name,
302            root_canister_id: &prepared.root_canister_id,
303            execution_context: &execution_context,
304        },
305    )
306}
307
308fn persist_install_result(
309    receipt_scope: InstallReceiptScope<'_>,
310    options: &InstallRootOptions,
311    workspace_root: &Path,
312    config_path: &Path,
313    manifest_path: &Path,
314    completion: InstallCompletion<'_>,
315) -> Result<(), InstallRootError> {
316    let state = build_install_state(
317        options,
318        workspace_root,
319        receipt_scope.icp_root,
320        config_path,
321        manifest_path,
322        (receipt_scope.deployment_name, completion.fleet_name),
323        completion.root_canister_id,
324    )
325    .map_err(InstallRootError::in_phase(
326        InstallRootPhase::StatePersistence,
327    ))?;
328    let state_path = write_install_state_with_deployment_truth_receipt(
329        receipt_scope,
330        &options.environment,
331        &state,
332    )
333    .map_err(InstallRootError::in_phase(
334        InstallRootPhase::StatePersistence,
335    ))?;
336    write_artifact_promotion_execution_receipt_for_install(
337        options,
338        receipt_scope.icp_root,
339        receipt_scope.environment,
340        receipt_scope.deployment_name,
341        receipt_scope.check,
342        completion.execution_context,
343    )
344    .map_err(InstallRootError::in_phase(
345        InstallRootPhase::ArtifactPromotion,
346    ))?;
347    print_install_result_summary(
348        receipt_scope.environment,
349        &state.deployment_name,
350        &state.fleet_template,
351        &state_path,
352    );
353    Ok(())
354}
355
356fn current_install_build_inputs(
357    workspace_root: &std::path::Path,
358    icp_root: &std::path::Path,
359    config_path: &std::path::Path,
360    options: &InstallRootOptions,
361) -> Result<
362    (
363        crate::canister_build::WorkspaceBuildContext,
364        build_snapshot::ValidatedInstallSnapshot,
365    ),
366    Box<dyn std::error::Error>,
367> {
368    let context = resolve_install_build_context(
369        workspace_root,
370        icp_root,
371        config_path,
372        &options.environment,
373        &options.root_build_target,
374        options.build_profile,
375    )?;
376    let snapshot = resolve_install_snapshot(
377        &context,
378        &options.root_build_target,
379        options.deployment_plan_override.is_some(),
380    )?;
381    Ok((context, snapshot))
382}