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