canic_backup/restore/apply/journal/receipts/
mod.rs1use super::{
8 RestoreApplyJournal, RestoreApplyJournalError, RestoreApplyJournalOperation,
9 RestoreApplyOperationKind, RestoreApplyRunnerCommand, validate_apply_journal_nonempty,
10};
11use crate::artifacts::ArtifactChecksum;
12
13use serde::{Deserialize, Serialize};
14
15#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
23#[serde(deny_unknown_fields)]
24pub struct RestoreApplyOperationReceipt {
25 pub sequence: usize,
26 pub operation: RestoreApplyOperationKind,
27 pub outcome: RestoreApplyOperationReceiptOutcome,
28 pub source_canister: String,
29 pub target_canister: String,
30 pub attempt: usize,
31 #[serde(deserialize_with = "crate::serialization::required_option")]
32 pub updated_at: Option<String>,
33 #[serde(deserialize_with = "crate::serialization::required_option")]
34 pub command: Option<RestoreApplyRunnerCommand>,
35 #[serde(deserialize_with = "crate::serialization::required_option")]
36 pub status: Option<String>,
37 #[serde(deserialize_with = "crate::serialization::required_option")]
38 pub stdout: Option<RestoreApplyCommandOutput>,
39 #[serde(deserialize_with = "crate::serialization::required_option")]
40 pub stderr: Option<RestoreApplyCommandOutput>,
41 #[serde(deserialize_with = "crate::serialization::required_option")]
42 pub failure_reason: Option<String>,
43 #[serde(deserialize_with = "crate::serialization::required_option")]
44 pub source_snapshot_id: Option<String>,
45 #[serde(deserialize_with = "crate::serialization::required_option")]
46 pub artifact_path: Option<String>,
47 #[serde(deserialize_with = "crate::serialization::required_option")]
48 pub artifact_checksum: Option<ArtifactChecksum>,
49 #[serde(deserialize_with = "crate::serialization::required_option")]
50 pub uploaded_snapshot_id: Option<String>,
51}
52
53impl RestoreApplyOperationReceipt {
54 #[must_use]
56 pub(in crate::restore) fn command_completed(
57 operation: &RestoreApplyJournalOperation,
58 command: RestoreApplyRunnerCommand,
59 status: String,
60 updated_at: Option<String>,
61 output: RestoreApplyCommandOutputPair,
62 attempt: usize,
63 uploaded_snapshot_id: Option<String>,
64 ) -> Self {
65 Self::from_operation(
66 operation,
67 operation.operation.clone(),
68 RestoreApplyOperationReceiptOutcome::CommandCompleted,
69 RestoreApplyOperationReceiptDetails {
70 attempt,
71 updated_at,
72 command: Some(command),
73 status: Some(status),
74 stdout: Some(output.stdout),
75 stderr: Some(output.stderr),
76 uploaded_snapshot_id,
77 failure_reason: None,
78 },
79 )
80 }
81
82 #[must_use]
84 pub(in crate::restore) fn command_failed(
85 operation: &RestoreApplyJournalOperation,
86 command: RestoreApplyRunnerCommand,
87 status: String,
88 updated_at: Option<String>,
89 output: RestoreApplyCommandOutputPair,
90 attempt: usize,
91 failure_reason: String,
92 ) -> Self {
93 Self::from_operation(
94 operation,
95 operation.operation.clone(),
96 RestoreApplyOperationReceiptOutcome::CommandFailed,
97 RestoreApplyOperationReceiptDetails {
98 attempt,
99 updated_at,
100 command: Some(command),
101 status: Some(status),
102 stdout: Some(output.stdout),
103 stderr: Some(output.stderr),
104 failure_reason: Some(failure_reason),
105 uploaded_snapshot_id: None,
106 },
107 )
108 }
109
110 fn from_operation(
111 operation: &RestoreApplyJournalOperation,
112 operation_kind: RestoreApplyOperationKind,
113 outcome: RestoreApplyOperationReceiptOutcome,
114 details: RestoreApplyOperationReceiptDetails,
115 ) -> Self {
116 Self {
117 sequence: operation.sequence,
118 operation: operation_kind,
119 outcome,
120 source_canister: operation.source_canister.clone(),
121 target_canister: operation.target_canister.clone(),
122 attempt: details.attempt,
123 updated_at: details.updated_at,
124 command: details.command,
125 status: details.status,
126 stdout: details.stdout,
127 stderr: details.stderr,
128 failure_reason: details.failure_reason,
129 source_snapshot_id: operation.snapshot_id.clone(),
130 artifact_path: operation.artifact_path.clone(),
131 artifact_checksum: operation.artifact_checksum.clone(),
132 uploaded_snapshot_id: details.uploaded_snapshot_id,
133 }
134 }
135
136 pub(super) fn matches_load_operation(&self, load: &RestoreApplyJournalOperation) -> bool {
137 self.operation == RestoreApplyOperationKind::UploadSnapshot
138 && self.outcome == RestoreApplyOperationReceiptOutcome::CommandCompleted
139 && load.operation == RestoreApplyOperationKind::LoadSnapshot
140 && self.source_canister == load.source_canister
141 && self.target_canister == load.target_canister
142 && self.source_snapshot_id == load.snapshot_id
143 && self.artifact_path == load.artifact_path
144 && self.artifact_checksum == load.artifact_checksum
145 && self
146 .uploaded_snapshot_id
147 .as_ref()
148 .is_some_and(|id| !id.trim().is_empty())
149 }
150
151 pub(super) fn validate_against(
152 &self,
153 journal: &RestoreApplyJournal,
154 ) -> Result<(), RestoreApplyJournalError> {
155 let operation = journal
156 .operations
157 .iter()
158 .find(|operation| operation.sequence == self.sequence)
159 .ok_or(RestoreApplyJournalError::OperationReceiptOperationNotFound(
160 self.sequence,
161 ))?;
162 if operation.operation != self.operation
163 || operation.source_canister != self.source_canister
164 || operation.target_canister != self.target_canister
165 {
166 return Err(RestoreApplyJournalError::OperationReceiptMismatch {
167 sequence: self.sequence,
168 });
169 }
170 validate_apply_journal_nonempty(
171 "operation_receipts[].updated_at",
172 self.updated_at.as_deref().unwrap_or_default(),
173 )?;
174 let command =
175 Self::validate_present("operation_receipts[].command", self.command.as_ref())?;
176 validate_apply_journal_nonempty("operation_receipts[].command.program", &command.program)?;
177 validate_apply_journal_nonempty("operation_receipts[].command.note", &command.note)?;
178 if command.args.is_empty() {
179 return Err(RestoreApplyJournalError::MissingField(
180 "operation_receipts[].command.args",
181 ));
182 }
183 validate_apply_journal_nonempty(
184 "operation_receipts[].status",
185 self.status.as_deref().unwrap_or_default(),
186 )?;
187 Self::validate_present("operation_receipts[].stdout", self.stdout.as_ref())?;
188 Self::validate_present("operation_receipts[].stderr", self.stderr.as_ref())?;
189 if self.operation == RestoreApplyOperationKind::UploadSnapshot {
190 validate_apply_journal_nonempty(
191 "operation_receipts[].source_snapshot_id",
192 self.source_snapshot_id.as_deref().unwrap_or_default(),
193 )?;
194 validate_apply_journal_nonempty(
195 "operation_receipts[].artifact_path",
196 self.artifact_path.as_deref().unwrap_or_default(),
197 )?;
198 if !self.matches_artifact_identity(operation) {
199 return Err(RestoreApplyJournalError::OperationReceiptMismatch {
200 sequence: self.sequence,
201 });
202 }
203 if self.outcome == RestoreApplyOperationReceiptOutcome::CommandCompleted {
204 validate_apply_journal_nonempty(
205 "operation_receipts[].uploaded_snapshot_id",
206 self.uploaded_snapshot_id.as_deref().unwrap_or_default(),
207 )?;
208 }
209 }
210 if self.outcome == RestoreApplyOperationReceiptOutcome::CommandFailed {
211 validate_apply_journal_nonempty(
212 "operation_receipts[].failure_reason",
213 self.failure_reason.as_deref().unwrap_or_default(),
214 )?;
215 }
216
217 Ok(())
218 }
219
220 fn matches_artifact_identity(&self, operation: &RestoreApplyJournalOperation) -> bool {
221 let snapshot_id = &operation.snapshot_id;
222 let artifact_path = &operation.artifact_path;
223 let artifact_checksum = &operation.artifact_checksum;
224 self.source_snapshot_id.as_deref() == snapshot_id.as_deref()
225 && self.artifact_path.as_deref() == artifact_path.as_deref()
226 && self.artifact_checksum.as_ref() == artifact_checksum.as_ref()
227 }
228
229 fn validate_present<'a, T>(
230 field: &'static str,
231 value: Option<&'a T>,
232 ) -> Result<&'a T, RestoreApplyJournalError> {
233 value.ok_or(RestoreApplyJournalError::MissingField(field))
234 }
235}
236
237#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
245#[serde(rename_all = "kebab-case")]
246pub enum RestoreApplyOperationReceiptOutcome {
247 CommandCompleted,
248 CommandFailed,
249}
250
251#[derive(Default)]
259struct RestoreApplyOperationReceiptDetails {
260 attempt: usize,
261 updated_at: Option<String>,
262 command: Option<RestoreApplyRunnerCommand>,
263 status: Option<String>,
264 stdout: Option<RestoreApplyCommandOutput>,
265 stderr: Option<RestoreApplyCommandOutput>,
266 failure_reason: Option<String>,
267 uploaded_snapshot_id: Option<String>,
268}
269
270#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
278#[serde(deny_unknown_fields)]
279pub struct RestoreApplyCommandOutput {
280 pub text: String,
281 pub truncated: bool,
282 pub original_bytes: usize,
283}
284
285impl RestoreApplyCommandOutput {
286 #[must_use]
288 pub(in crate::restore) fn from_bytes(bytes: &[u8], limit: usize) -> Self {
289 let original_bytes = bytes.len();
290 let start = original_bytes.saturating_sub(limit);
291 Self {
292 text: String::from_utf8_lossy(&bytes[start..]).to_string(),
293 truncated: start > 0,
294 original_bytes,
295 }
296 }
297}
298
299#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
304#[serde(deny_unknown_fields)]
305pub(in crate::restore) struct RestoreApplyCommandOutputPair {
306 pub stdout: RestoreApplyCommandOutput,
307 pub stderr: RestoreApplyCommandOutput,
308}
309
310impl RestoreApplyCommandOutputPair {
311 #[must_use]
313 pub(in crate::restore) fn from_bytes(stdout: &[u8], stderr: &[u8], limit: usize) -> Self {
314 Self {
315 stdout: RestoreApplyCommandOutput::from_bytes(stdout, limit),
316 stderr: RestoreApplyCommandOutput::from_bytes(stderr, limit),
317 }
318 }
319}