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