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#[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 #[must_use]
36 pub fn operation_error(&self) -> &(dyn Error + 'static) {
37 self.operation.as_ref()
38 }
39
40 #[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}