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