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) 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}