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