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) network: &'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) fn write_completed_install_phase_receipt(
66    receipt_scope: InstallReceiptScope<'_>,
67    completed: CompletedInstallPhase,
68) -> Result<PathBuf, Box<dyn std::error::Error>> {
69    let role_phase_receipts = completed
70        .role_names
71        .iter()
72        .filter_map(|role| {
73            completed_phase_role_receipt(
74                receipt_scope.check,
75                completed.phase,
76                role,
77                RolePhaseResultV1::Applied,
78                None,
79            )
80        })
81        .collect();
82    let receipt =
83        receipt_scope.with_execution_context(install_deployment_truth_phase_receipt_with_result(
84            receipt_scope.check,
85            PhaseReceiptInput {
86                phase: completed.phase,
87                started_at: completed.started_at,
88                finished_at: completed.finished_at,
89                attempted_action: completed.attempted_action,
90                status: ObservationStatusV1::Observed,
91                evidence: completed.evidence,
92                role_phase_receipts,
93                operation_status: DeploymentExecutionStatusV1::Complete,
94                command_result: DeploymentCommandResultV1::Succeeded,
95            },
96        ));
97    receipt_scope.write_receipt(&receipt)
98}
99
100pub(super) fn completed_phase_role_receipt(
101    check: &DeploymentCheckV1,
102    phase: InstallPhaseLabel,
103    role: &str,
104    result: RolePhaseResultV1,
105    error: Option<String>,
106) -> Option<RolePhaseReceiptV1> {
107    let planned = check
108        .plan
109        .role_artifacts
110        .iter()
111        .find(|artifact| artifact.role == role)?;
112    let observed = check
113        .inventory
114        .observed_artifacts
115        .iter()
116        .find(|artifact| artifact.role == role);
117    let artifact_digest = observed
118        .and_then(|artifact| artifact.file_sha256.clone())
119        .or_else(|| observed.and_then(|artifact| artifact.payload_sha256.clone()))
120        .or_else(|| planned.observed_wasm_gz_file_sha256.clone())
121        .or_else(|| planned.wasm_gz_sha256.clone());
122
123    Some(RolePhaseReceiptV1 {
124        role: role.to_string(),
125        phase: phase.as_str().to_string(),
126        result,
127        previous_module_hash: None,
128        target_module_hash: planned.installed_module_hash.clone(),
129        observed_module_hash_after: None,
130        artifact_digest,
131        canonical_embedded_config_sha256: planned.canonical_embedded_config_sha256.clone(),
132        error,
133    })
134}
135
136pub(super) fn install_deployment_truth_phase_receipt(
137    check: &DeploymentCheckV1,
138    phase: InstallPhaseLabel,
139    started_at: String,
140    finished_at: Option<String>,
141    attempted_action: &str,
142    status: ObservationStatusV1,
143    evidence: Vec<String>,
144) -> DeploymentReceiptV1 {
145    install_deployment_truth_phase_receipt_with_result(
146        check,
147        PhaseReceiptInput {
148            phase,
149            started_at,
150            finished_at,
151            attempted_action,
152            status,
153            evidence,
154            role_phase_receipts: Vec::new(),
155            operation_status: DeploymentExecutionStatusV1::Complete,
156            command_result: DeploymentCommandResultV1::Succeeded,
157        },
158    )
159}
160
161fn install_deployment_truth_phase_receipt_with_result(
162    check: &DeploymentCheckV1,
163    input: PhaseReceiptInput<'_>,
164) -> DeploymentReceiptV1 {
165    deployment_receipt_from_check_with_status(
166        check,
167        format!("{}:{}", check.check_id, input.phase.as_str()),
168        input.operation_status,
169        input.started_at.clone(),
170        input.finished_at.clone(),
171        vec![phase_receipt(
172            input.phase.as_str(),
173            input.started_at,
174            input.finished_at,
175            input.attempted_action,
176            input.status,
177            input.evidence,
178        )],
179        input.role_phase_receipts,
180        input.command_result,
181    )
182}
183
184pub(super) fn receipt_with_execution_context(
185    mut receipt: DeploymentReceiptV1,
186    execution_context: &DeploymentExecutionContextV1,
187) -> DeploymentReceiptV1 {
188    receipt.execution_context = Some(execution_context.clone());
189    receipt
190}
191
192struct PhaseReceiptInput<'a> {
193    phase: InstallPhaseLabel,
194    started_at: String,
195    finished_at: Option<String>,
196    attempted_action: &'a str,
197    status: ObservationStatusV1,
198    evidence: Vec<String>,
199    role_phase_receipts: Vec<RolePhaseReceiptV1>,
200    operation_status: DeploymentExecutionStatusV1,
201    command_result: DeploymentCommandResultV1,
202}
203
204impl InstallReceiptScope<'_> {
205    pub(super) fn run_operation(
206        self,
207        operation: &impl InstallPhaseOperation,
208    ) -> Result<Duration, Box<dyn std::error::Error>> {
209        self.run_phase(
210            operation.phase(),
211            operation.attempted_action(),
212            operation.evidence(),
213            || operation.execute(),
214        )
215    }
216
217    pub(super) fn run_phase(
218        self,
219        phase: InstallPhaseLabel,
220        attempted_action: &str,
221        evidence: Vec<String>,
222        run: impl FnOnce() -> Result<(), Box<dyn std::error::Error>>,
223    ) -> Result<Duration, Box<dyn std::error::Error>> {
224        let started_at = current_unix_timestamp_label()?;
225        let started = Instant::now();
226        match run() {
227            Ok(()) => {
228                let duration = started.elapsed();
229                let receipt = self.with_execution_context(install_deployment_truth_phase_receipt(
230                    self.check,
231                    phase,
232                    started_at,
233                    Some(current_unix_timestamp_label()?),
234                    attempted_action,
235                    ObservationStatusV1::Observed,
236                    evidence,
237                ));
238                self.write_receipt(&receipt)?;
239                Ok(duration)
240            }
241            Err(err) => {
242                if let Err(receipt_write) = self.write_failed_phase_receipt(
243                    phase,
244                    started_at,
245                    attempted_action,
246                    evidence,
247                    err.as_ref(),
248                ) {
249                    return Err(Box::new(InstallPhaseFailureError {
250                        operation: err,
251                        receipt_write,
252                    }));
253                }
254                Err(err)
255            }
256        }
257    }
258
259    pub(super) fn with_execution_context(
260        self,
261        receipt: DeploymentReceiptV1,
262    ) -> DeploymentReceiptV1 {
263        match self.execution_context {
264            Some(context) => receipt_with_execution_context(receipt, context),
265            None => receipt,
266        }
267    }
268
269    fn write_receipt(
270        self,
271        receipt: &DeploymentReceiptV1,
272    ) -> Result<PathBuf, Box<dyn std::error::Error>> {
273        let path = write_install_deployment_truth_receipt(
274            self.icp_root,
275            self.network,
276            self.deployment_name,
277            receipt,
278        )?;
279        println!("Deployment truth receipt JSON: {}", path.display());
280        Ok(path)
281    }
282
283    fn write_failed_phase_receipt(
284        self,
285        phase: InstallPhaseLabel,
286        started_at: String,
287        attempted_action: &str,
288        evidence: Vec<String>,
289        err: &dyn std::error::Error,
290    ) -> Result<(), Box<dyn Error>> {
291        let receipt = install_deployment_truth_phase_receipt_with_result(
292            self.check,
293            PhaseReceiptInput {
294                phase,
295                started_at,
296                finished_at: Some(current_unix_timestamp_label()?),
297                attempted_action,
298                status: ObservationStatusV1::Inconclusive,
299                evidence,
300                role_phase_receipts: Vec::new(),
301                operation_status: DeploymentExecutionStatusV1::FailedAfterMutation,
302                command_result: DeploymentCommandResultV1::Failed {
303                    code: format!("{}_failed", phase.as_str()),
304                    message: err.to_string(),
305                },
306            },
307        );
308        let receipt = self.with_execution_context(receipt);
309        self.write_receipt(&receipt)?;
310        Ok(())
311    }
312}