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