Skip to main content

canic_host/install_root/
phase_receipts.rs

1use super::clock::current_unix_timestamp_label;
2use super::operations::{InstallPhaseLabel, InstallPhaseOperation};
3use super::receipt_io::write_install_deployment_truth_receipt;
4use crate::deployment_truth::{
5    DeploymentCheckV1, DeploymentCommandResultV1, DeploymentExecutionContextV1,
6    DeploymentExecutionStatusV1, DeploymentReceiptV1, ObservationStatusV1, RolePhaseReceiptV1,
7    RolePhaseResultV1, deployment_receipt_from_check_with_status, phase_receipt,
8};
9use canic_core::ids::FleetKey;
10use std::{
11    error::Error,
12    path::{Path, PathBuf},
13    time::{Duration, Instant},
14};
15use thiserror::Error as ThisError;
16
17///
18/// InstallPhaseFailureError
19///
20/// Host error preserving both the phase operation error and the independent
21/// failure-receipt persistence error. Owned by the install receipt adapter and
22/// exposed through root-install failures.
23///
24#[derive(Debug, ThisError)]
25#[error(
26    "install phase operation failed and its failure receipt could not be written: operation={operation}; receipt_write={receipt_write}"
27)]
28pub struct InstallPhaseFailureError {
29    #[source]
30    operation: Box<dyn Error>,
31    receipt_write: Box<dyn Error>,
32}
33
34impl InstallPhaseFailureError {
35    /// Return the original phase operation failure.
36    #[must_use]
37    pub fn operation_error(&self) -> &(dyn Error + 'static) {
38        self.operation.as_ref()
39    }
40
41    /// Return the independent failure-receipt persistence failure.
42    #[must_use]
43    pub fn receipt_write_error(&self) -> &(dyn Error + 'static) {
44        self.receipt_write.as_ref()
45    }
46}
47
48#[derive(Clone, Copy)]
49pub(super) struct InstallReceiptScope<'a> {
50    pub(super) icp_root: &'a Path,
51    pub(super) environment: &'a str,
52    pub(super) fleet: FleetKey,
53    pub(super) check: &'a DeploymentCheckV1,
54    pub(super) execution_context: Option<&'a DeploymentExecutionContextV1>,
55}
56
57pub(super) struct CompletedInstallPhase {
58    pub(super) phase: InstallPhaseLabel,
59    pub(super) attempted_action: &'static str,
60    pub(super) started_at: String,
61    pub(super) finished_at: Option<String>,
62    pub(super) evidence: Vec<String>,
63    pub(super) role_names: Vec<String>,
64}
65
66pub(super) struct CompletedInstallOperation {
67    pub(super) duration: Duration,
68    pub(super) receipt_path: PathBuf,
69}
70
71pub(super) fn write_completed_install_phase_receipt(
72    receipt_scope: InstallReceiptScope<'_>,
73    completed: CompletedInstallPhase,
74) -> Result<PathBuf, Box<dyn std::error::Error>> {
75    let role_phase_receipts = completed
76        .role_names
77        .iter()
78        .filter_map(|role| {
79            completed_phase_role_receipt(
80                receipt_scope.check,
81                completed.phase,
82                role,
83                RolePhaseResultV1::Applied,
84                None,
85            )
86        })
87        .collect();
88    let receipt =
89        receipt_scope.with_execution_context(install_deployment_truth_phase_receipt_with_result(
90            receipt_scope.check,
91            PhaseReceiptInput {
92                phase: completed.phase,
93                started_at: completed.started_at,
94                finished_at: completed.finished_at,
95                attempted_action: completed.attempted_action,
96                status: ObservationStatusV1::Observed,
97                evidence: completed.evidence,
98                role_phase_receipts,
99                operation_status: DeploymentExecutionStatusV1::Complete,
100                command_result: DeploymentCommandResultV1::Succeeded,
101            },
102        ));
103    receipt_scope.write_receipt(&receipt)
104}
105
106pub(super) fn completed_phase_role_receipt(
107    check: &DeploymentCheckV1,
108    phase: InstallPhaseLabel,
109    role: &str,
110    result: RolePhaseResultV1,
111    error: Option<String>,
112) -> Option<RolePhaseReceiptV1> {
113    let planned = check
114        .plan
115        .role_artifacts
116        .iter()
117        .find(|artifact| artifact.role == role)?;
118    let observed = check
119        .inventory
120        .observed_artifacts
121        .iter()
122        .find(|artifact| artifact.role == role);
123    let artifact_digest = observed
124        .and_then(|artifact| artifact.file_sha256.clone())
125        .or_else(|| observed.and_then(|artifact| artifact.payload_sha256.clone()))
126        .or_else(|| planned.observed_wasm_gz_file_sha256.clone())
127        .or_else(|| planned.wasm_gz_sha256.clone());
128
129    Some(RolePhaseReceiptV1 {
130        role: role.to_string(),
131        phase: phase.as_str().to_string(),
132        result,
133        previous_module_hash: None,
134        target_module_hash: planned.installed_module_hash.clone(),
135        observed_module_hash_after: None,
136        artifact_digest,
137        canonical_embedded_config_sha256: planned.canonical_embedded_config_sha256.clone(),
138        error,
139    })
140}
141
142pub(super) fn install_deployment_truth_phase_receipt(
143    check: &DeploymentCheckV1,
144    phase: InstallPhaseLabel,
145    started_at: String,
146    finished_at: Option<String>,
147    attempted_action: &str,
148    status: ObservationStatusV1,
149    evidence: Vec<String>,
150) -> DeploymentReceiptV1 {
151    install_deployment_truth_phase_receipt_with_result(
152        check,
153        PhaseReceiptInput {
154            phase,
155            started_at,
156            finished_at,
157            attempted_action,
158            status,
159            evidence,
160            role_phase_receipts: Vec::new(),
161            operation_status: DeploymentExecutionStatusV1::Complete,
162            command_result: DeploymentCommandResultV1::Succeeded,
163        },
164    )
165}
166
167fn install_deployment_truth_phase_receipt_with_result(
168    check: &DeploymentCheckV1,
169    input: PhaseReceiptInput<'_>,
170) -> DeploymentReceiptV1 {
171    deployment_receipt_from_check_with_status(
172        check,
173        format!("{}:{}", check.check_id, input.phase.as_str()),
174        input.operation_status,
175        input.started_at.clone(),
176        input.finished_at.clone(),
177        vec![phase_receipt(
178            input.phase.as_str(),
179            input.started_at,
180            input.finished_at,
181            input.attempted_action,
182            input.status,
183            input.evidence,
184        )],
185        input.role_phase_receipts,
186        input.command_result,
187    )
188}
189
190pub(super) fn receipt_with_execution_context(
191    mut receipt: DeploymentReceiptV1,
192    execution_context: &DeploymentExecutionContextV1,
193) -> DeploymentReceiptV1 {
194    receipt.execution_context = Some(execution_context.clone());
195    receipt
196}
197
198struct PhaseReceiptInput<'a> {
199    phase: InstallPhaseLabel,
200    started_at: String,
201    finished_at: Option<String>,
202    attempted_action: &'a str,
203    status: ObservationStatusV1,
204    evidence: Vec<String>,
205    role_phase_receipts: Vec<RolePhaseReceiptV1>,
206    operation_status: DeploymentExecutionStatusV1,
207    command_result: DeploymentCommandResultV1,
208}
209
210impl InstallReceiptScope<'_> {
211    pub(super) fn run_operation_with_receipt(
212        self,
213        operation: &impl InstallPhaseOperation,
214        root_principal: Option<&str>,
215    ) -> Result<CompletedInstallOperation, Box<dyn std::error::Error>> {
216        let attempted_evidence = operation.evidence();
217        self.run_phase_with_receipt(
218            operation.phase(),
219            operation.attempted_action(),
220            attempted_evidence,
221            root_principal,
222            || operation.execute_and_verify(),
223        )
224    }
225
226    fn run_phase_with_receipt(
227        self,
228        phase: InstallPhaseLabel,
229        attempted_action: &str,
230        attempted_evidence: Vec<String>,
231        root_principal: Option<&str>,
232        run: impl FnOnce() -> Result<Vec<String>, Box<dyn std::error::Error>>,
233    ) -> Result<CompletedInstallOperation, Box<dyn std::error::Error>> {
234        let started_at = current_unix_timestamp_label()?;
235        let started = Instant::now();
236        match run() {
237            Ok(verified_evidence) => {
238                let duration = started.elapsed();
239                let mut receipt =
240                    self.with_execution_context(install_deployment_truth_phase_receipt(
241                        self.check,
242                        phase,
243                        started_at,
244                        Some(current_unix_timestamp_label()?),
245                        attempted_action,
246                        ObservationStatusV1::Observed,
247                        verified_evidence,
248                    ));
249                if let Some(root_principal) = root_principal {
250                    receipt.root_principal = Some(root_principal.to_string());
251                }
252                let receipt_path = self.write_receipt(&receipt)?;
253                Ok(CompletedInstallOperation {
254                    duration,
255                    receipt_path,
256                })
257            }
258            Err(err) => {
259                if let Err(receipt_write) = self.write_failed_phase_receipt(
260                    phase,
261                    started_at,
262                    attempted_action,
263                    attempted_evidence,
264                    err.as_ref(),
265                ) {
266                    return Err(Box::new(InstallPhaseFailureError {
267                        operation: err,
268                        receipt_write,
269                    }));
270                }
271                Err(err)
272            }
273        }
274    }
275
276    pub(super) fn with_execution_context(
277        self,
278        receipt: DeploymentReceiptV1,
279    ) -> DeploymentReceiptV1 {
280        match self.execution_context {
281            Some(context) => receipt_with_execution_context(receipt, context),
282            None => receipt,
283        }
284    }
285
286    pub(super) fn write_receipt(
287        self,
288        receipt: &DeploymentReceiptV1,
289    ) -> Result<PathBuf, Box<dyn std::error::Error>> {
290        let path = write_install_deployment_truth_receipt(self.icp_root, self.fleet, receipt)?;
291        println!("Deployment truth receipt JSON: {}", path.display());
292        Ok(path)
293    }
294
295    fn write_failed_phase_receipt(
296        self,
297        phase: InstallPhaseLabel,
298        started_at: String,
299        attempted_action: &str,
300        evidence: Vec<String>,
301        err: &dyn std::error::Error,
302    ) -> Result<(), Box<dyn Error>> {
303        let receipt = install_deployment_truth_phase_receipt_with_result(
304            self.check,
305            PhaseReceiptInput {
306                phase,
307                started_at,
308                finished_at: Some(current_unix_timestamp_label()?),
309                attempted_action,
310                status: ObservationStatusV1::Inconclusive,
311                evidence,
312                role_phase_receipts: Vec::new(),
313                operation_status: DeploymentExecutionStatusV1::FailedAfterMutation,
314                command_result: DeploymentCommandResultV1::Failed {
315                    code: format!("{}_failed", phase.as_str()),
316                    message: err.to_string(),
317                },
318            },
319        );
320        let receipt = self.with_execution_context(receipt);
321        self.write_receipt(&receipt)?;
322        Ok(())
323    }
324}