use super::{
RestoreApplyJournal, RestoreApplyJournalError, RestoreApplyJournalOperation,
RestoreApplyOperationKind, RestoreApplyRunnerCommand, validate_apply_journal_nonempty,
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RestoreApplyOperationReceipt {
pub sequence: usize,
pub operation: RestoreApplyOperationKind,
#[serde(default)]
pub outcome: RestoreApplyOperationReceiptOutcome,
pub source_canister: String,
pub target_canister: String,
#[serde(default)]
pub attempt: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub command: Option<RestoreApplyRunnerCommand>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stdout: Option<RestoreApplyCommandOutput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stderr: Option<RestoreApplyCommandOutput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub failure_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_snapshot_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub artifact_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uploaded_snapshot_id: Option<String>,
}
impl RestoreApplyOperationReceipt {
#[must_use]
pub(in crate::restore) fn command_completed(
operation: &RestoreApplyJournalOperation,
command: RestoreApplyRunnerCommand,
status: String,
updated_at: Option<String>,
output: RestoreApplyCommandOutputPair,
attempt: usize,
uploaded_snapshot_id: Option<String>,
) -> Self {
Self::from_operation(
operation,
operation.operation.clone(),
RestoreApplyOperationReceiptOutcome::CommandCompleted,
RestoreApplyOperationReceiptDetails {
attempt,
updated_at,
command: Some(command),
status: Some(status),
stdout: Some(output.stdout),
stderr: Some(output.stderr),
uploaded_snapshot_id,
failure_reason: None,
},
)
}
#[must_use]
pub(in crate::restore) fn command_failed(
operation: &RestoreApplyJournalOperation,
command: RestoreApplyRunnerCommand,
status: String,
updated_at: Option<String>,
output: RestoreApplyCommandOutputPair,
attempt: usize,
failure_reason: String,
) -> Self {
Self::from_operation(
operation,
operation.operation.clone(),
RestoreApplyOperationReceiptOutcome::CommandFailed,
RestoreApplyOperationReceiptDetails {
attempt,
updated_at,
command: Some(command),
status: Some(status),
stdout: Some(output.stdout),
stderr: Some(output.stderr),
failure_reason: Some(failure_reason),
uploaded_snapshot_id: None,
},
)
}
fn from_operation(
operation: &RestoreApplyJournalOperation,
operation_kind: RestoreApplyOperationKind,
outcome: RestoreApplyOperationReceiptOutcome,
details: RestoreApplyOperationReceiptDetails,
) -> Self {
Self {
sequence: operation.sequence,
operation: operation_kind,
outcome,
source_canister: operation.source_canister.clone(),
target_canister: operation.target_canister.clone(),
attempt: details.attempt,
updated_at: details.updated_at,
command: details.command,
status: details.status,
stdout: details.stdout,
stderr: details.stderr,
failure_reason: details.failure_reason,
source_snapshot_id: operation.snapshot_id.clone(),
artifact_path: operation.artifact_path.clone(),
uploaded_snapshot_id: details.uploaded_snapshot_id,
}
}
pub(super) fn matches_load_operation(&self, load: &RestoreApplyJournalOperation) -> bool {
self.operation == RestoreApplyOperationKind::UploadSnapshot
&& self.outcome == RestoreApplyOperationReceiptOutcome::CommandCompleted
&& load.operation == RestoreApplyOperationKind::LoadSnapshot
&& self.source_canister == load.source_canister
&& self.target_canister == load.target_canister
&& self.source_snapshot_id == load.snapshot_id
&& self.artifact_path == load.artifact_path
&& self
.uploaded_snapshot_id
.as_ref()
.is_some_and(|id| !id.trim().is_empty())
}
pub(super) fn validate_against(
&self,
journal: &RestoreApplyJournal,
) -> Result<(), RestoreApplyJournalError> {
let operation = journal
.operations
.iter()
.find(|operation| operation.sequence == self.sequence)
.ok_or(RestoreApplyJournalError::OperationReceiptOperationNotFound(
self.sequence,
))?;
if operation.operation != self.operation
|| operation.source_canister != self.source_canister
|| operation.target_canister != self.target_canister
{
return Err(RestoreApplyJournalError::OperationReceiptMismatch {
sequence: self.sequence,
});
}
if self.operation == RestoreApplyOperationKind::UploadSnapshot {
validate_apply_journal_nonempty(
"operation_receipts[].source_snapshot_id",
self.source_snapshot_id.as_deref().unwrap_or_default(),
)?;
validate_apply_journal_nonempty(
"operation_receipts[].artifact_path",
self.artifact_path.as_deref().unwrap_or_default(),
)?;
if self.outcome == RestoreApplyOperationReceiptOutcome::CommandCompleted {
validate_apply_journal_nonempty(
"operation_receipts[].uploaded_snapshot_id",
self.uploaded_snapshot_id.as_deref().unwrap_or_default(),
)?;
}
}
if self.outcome == RestoreApplyOperationReceiptOutcome::CommandFailed {
validate_apply_journal_nonempty(
"operation_receipts[].failure_reason",
self.failure_reason.as_deref().unwrap_or_default(),
)?;
}
Ok(())
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum RestoreApplyOperationReceiptOutcome {
#[default]
CommandCompleted,
CommandFailed,
}
#[derive(Default)]
struct RestoreApplyOperationReceiptDetails {
attempt: usize,
updated_at: Option<String>,
command: Option<RestoreApplyRunnerCommand>,
status: Option<String>,
stdout: Option<RestoreApplyCommandOutput>,
stderr: Option<RestoreApplyCommandOutput>,
failure_reason: Option<String>,
uploaded_snapshot_id: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RestoreApplyCommandOutput {
pub text: String,
pub truncated: bool,
pub original_bytes: usize,
}
impl RestoreApplyCommandOutput {
#[must_use]
pub(in crate::restore) fn from_bytes(bytes: &[u8], limit: usize) -> Self {
let original_bytes = bytes.len();
let start = original_bytes.saturating_sub(limit);
Self {
text: String::from_utf8_lossy(&bytes[start..]).to_string(),
truncated: start > 0,
original_bytes,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub(in crate::restore) struct RestoreApplyCommandOutputPair {
pub stdout: RestoreApplyCommandOutput,
pub stderr: RestoreApplyCommandOutput,
}
impl RestoreApplyCommandOutputPair {
#[must_use]
pub(in crate::restore) fn from_bytes(stdout: &[u8], stderr: &[u8], limit: usize) -> Self {
Self {
stdout: RestoreApplyCommandOutput::from_bytes(stdout, limit),
stderr: RestoreApplyCommandOutput::from_bytes(stderr, limit),
}
}
}