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