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