use super::{RestorePlan, RestorePlanError, RestorePlanMember};
use crate::{
artifacts::{ArtifactChecksum, ArtifactChecksumError},
manifest::VerificationCheck,
persistence::resolve_backup_artifact_path,
};
use serde::{Deserialize, Serialize};
use std::{
fs,
path::{Path, PathBuf},
};
use thiserror::Error as ThisError;
mod journal;
mod validation;
pub(in crate::restore) use journal::RestoreApplyCommandOutputPair;
pub(in crate::restore) use journal::RestoreApplyJournalReport;
pub use journal::{
RestoreApplyCommandConfig, RestoreApplyCommandOutput, RestoreApplyCommandPreview,
RestoreApplyJournal, RestoreApplyJournalError, RestoreApplyJournalOperation,
RestoreApplyOperationKind, RestoreApplyOperationKindCounts, RestoreApplyOperationReceipt,
RestoreApplyOperationReceiptOutcome, RestoreApplyOperationState, RestoreApplyPendingSummary,
RestoreApplyProgressSummary, RestoreApplyReportOperation, RestoreApplyReportOutcome,
RestoreApplyRunnerCommand,
};
pub use validation::RestoreApplyDryRunValidationError;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RestoreApplyDryRun {
pub dry_run_version: u16,
pub backup_id: String,
pub ready: bool,
pub readiness_reasons: Vec<String>,
pub member_count: usize,
pub planned_canister_stops: usize,
pub planned_canister_starts: usize,
pub planned_snapshot_uploads: usize,
pub planned_snapshot_loads: usize,
pub planned_verification_checks: usize,
pub planned_operations: usize,
pub rendered_operations: usize,
pub operation_counts: RestoreApplyOperationKindCounts,
pub artifact_validation: Option<RestoreApplyArtifactValidation>,
pub operations: Vec<RestoreApplyDryRunOperation>,
}
impl RestoreApplyDryRun {
pub fn from_plan(plan: &RestorePlan) -> Result<Self, RestorePlanError> {
plan.validate()?;
Ok(Self::from_validated_plan(plan))
}
pub fn try_from_plan_with_artifacts(
plan: &RestorePlan,
backup_root: &Path,
) -> Result<Self, RestoreApplyDryRunError> {
let mut dry_run = Self::from_plan(plan)?;
dry_run.artifact_validation = Some(validate_restore_apply_artifacts(plan, backup_root)?);
Ok(dry_run)
}
fn from_validated_plan(plan: &RestorePlan) -> Self {
let mut next_sequence = 0;
let ordered_members = plan.ordered_members();
let mut operations = Vec::new();
append_member_phase(
&mut operations,
&mut next_sequence,
RestoreApplyOperationKind::UploadSnapshot,
ordered_members.iter().copied(),
);
append_member_phase(
&mut operations,
&mut next_sequence,
RestoreApplyOperationKind::StopCanister,
ordered_members.iter().copied(),
);
append_member_phase(
&mut operations,
&mut next_sequence,
RestoreApplyOperationKind::LoadSnapshot,
ordered_members.iter().copied(),
);
append_member_phase(
&mut operations,
&mut next_sequence,
RestoreApplyOperationKind::StartCanister,
ordered_members.iter().rev().copied(),
);
append_member_verification_operations(
&mut operations,
&mut next_sequence,
&ordered_members,
);
append_deployment_verification_operations(plan, &mut operations, &mut next_sequence);
let rendered_operations = operations.len();
let operation_counts =
RestoreApplyOperationKindCounts::from_dry_run_operations(&operations);
Self {
dry_run_version: 1,
backup_id: plan.backup_id.clone(),
ready: plan.readiness_summary.ready,
readiness_reasons: plan.readiness_summary.reasons.clone(),
member_count: plan.member_count,
planned_canister_stops: plan.operation_summary.planned_canister_stops,
planned_canister_starts: plan.operation_summary.planned_canister_starts,
planned_snapshot_uploads: plan.operation_summary.planned_snapshot_uploads,
planned_snapshot_loads: plan.operation_summary.planned_snapshot_loads,
planned_verification_checks: plan.operation_summary.planned_verification_checks,
planned_operations: plan.operation_summary.planned_operations,
rendered_operations,
operation_counts,
artifact_validation: None,
operations,
}
}
}
fn validate_restore_apply_artifacts(
plan: &RestorePlan,
backup_root: &Path,
) -> Result<RestoreApplyArtifactValidation, RestoreApplyDryRunError> {
let backup_root = canonical_restore_backup_root(backup_root)?;
let mut checks = Vec::new();
for member in plan.ordered_members() {
checks.push(validate_restore_apply_artifact(member, &backup_root)?);
}
let members_with_expected_checksums = checks
.iter()
.filter(|check| check.checksum_expected.is_some())
.count();
let artifacts_present = checks.iter().all(|check| check.exists);
let checksums_verified = members_with_expected_checksums == plan.member_count
&& checks.iter().all(|check| check.checksum_verified);
Ok(RestoreApplyArtifactValidation {
backup_root: backup_root.to_string_lossy().to_string(),
checked_members: checks.len(),
artifacts_present,
checksums_verified,
members_with_expected_checksums,
checks,
})
}
fn validate_restore_apply_artifact(
member: &RestorePlanMember,
backup_root: &Path,
) -> Result<RestoreApplyArtifactCheck, RestoreApplyDryRunError> {
let resolved_path = safe_restore_artifact_path(
backup_root,
&member.source_canister,
&member.source_snapshot.artifact_path,
)?;
validate_restore_artifact_type(
backup_root,
&member.source_canister,
&member.source_snapshot.artifact_path,
&resolved_path,
)?;
let (checksum_actual, checksum_verified) =
if let Some(expected) = &member.source_snapshot.checksum {
let checksum = ArtifactChecksum::from_relative_path_no_follow(
backup_root,
Path::new(&member.source_snapshot.artifact_path),
)
.map_err(|source| RestoreApplyDryRunError::ArtifactChecksum {
source_canister: member.source_canister.clone(),
artifact_path: member.source_snapshot.artifact_path.clone(),
source,
})?;
checksum.verify(expected).map_err(|source| {
RestoreApplyDryRunError::ArtifactChecksum {
source_canister: member.source_canister.clone(),
artifact_path: member.source_snapshot.artifact_path.clone(),
source,
}
})?;
(Some(checksum.hash), true)
} else {
(None, false)
};
Ok(RestoreApplyArtifactCheck {
source_canister: member.source_canister.clone(),
target_canister: member.target_canister.clone(),
snapshot_id: member.source_snapshot.snapshot_id.clone(),
artifact_path: member.source_snapshot.artifact_path.clone(),
resolved_path: resolved_path.to_string_lossy().to_string(),
exists: true,
checksum_algorithm: member.source_snapshot.checksum_algorithm.clone(),
checksum_expected: member.source_snapshot.checksum.clone(),
checksum_actual,
checksum_verified,
})
}
fn canonical_restore_backup_root(backup_root: &Path) -> Result<PathBuf, RestoreApplyDryRunError> {
let metadata = fs::symlink_metadata(backup_root).map_err(|source| {
RestoreApplyDryRunError::ArtifactRootIo {
backup_root: backup_root.to_path_buf(),
source,
}
})?;
if metadata.file_type().is_symlink() || !metadata.is_dir() {
return Err(RestoreApplyDryRunError::ArtifactRootUnsafe {
backup_root: backup_root.to_path_buf(),
});
}
backup_root
.canonicalize()
.map_err(|source| RestoreApplyDryRunError::ArtifactRootIo {
backup_root: backup_root.to_path_buf(),
source,
})
}
fn validate_restore_artifact_type(
backup_root: &Path,
source_canister: &str,
artifact_path: &str,
resolved_path: &Path,
) -> Result<(), RestoreApplyDryRunError> {
let mut current = backup_root.to_path_buf();
let mut artifact_metadata = None;
for component in Path::new(artifact_path).components() {
current.push(component);
let metadata = match fs::symlink_metadata(¤t) {
Ok(metadata) => metadata,
Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
return Err(RestoreApplyDryRunError::ArtifactMissing {
source_canister: source_canister.to_string(),
artifact_path: artifact_path.to_string(),
resolved_path: resolved_path.to_string_lossy().to_string(),
});
}
Err(source) => {
return Err(RestoreApplyDryRunError::ArtifactChecksum {
source_canister: source_canister.to_string(),
artifact_path: artifact_path.to_string(),
source: ArtifactChecksumError::Io(source),
});
}
};
if metadata.file_type().is_symlink() {
return Err(RestoreApplyDryRunError::ArtifactUnsafeType {
source_canister: source_canister.to_string(),
artifact_path: current.to_string_lossy().to_string(),
kind: "symlink".to_string(),
});
}
artifact_metadata = Some(metadata);
}
let metadata =
artifact_metadata.ok_or_else(|| RestoreApplyDryRunError::ArtifactPathEscapesBackup {
source_canister: source_canister.to_string(),
artifact_path: artifact_path.to_string(),
})?;
if metadata.is_file() || metadata.is_dir() {
Ok(())
} else {
Err(RestoreApplyDryRunError::ArtifactUnsafeType {
source_canister: source_canister.to_string(),
artifact_path: artifact_path.to_string(),
kind: "special".to_string(),
})
}
}
fn safe_restore_artifact_path(
backup_root: &Path,
source_canister: &str,
artifact_path: &str,
) -> Result<PathBuf, RestoreApplyDryRunError> {
if let Some(path) = resolve_backup_artifact_path(backup_root, artifact_path) {
return Ok(path);
}
Err(RestoreApplyDryRunError::ArtifactPathEscapesBackup {
source_canister: source_canister.to_string(),
artifact_path: artifact_path.to_string(),
})
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RestoreApplyArtifactValidation {
pub backup_root: String,
pub checked_members: usize,
pub artifacts_present: bool,
pub checksums_verified: bool,
pub members_with_expected_checksums: usize,
pub checks: Vec<RestoreApplyArtifactCheck>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RestoreApplyArtifactCheck {
pub source_canister: String,
pub target_canister: String,
pub snapshot_id: String,
pub artifact_path: String,
pub resolved_path: String,
pub exists: bool,
pub checksum_algorithm: String,
pub checksum_expected: Option<String>,
pub checksum_actual: Option<String>,
pub checksum_verified: bool,
}
fn append_member_phase<'a>(
operations: &mut Vec<RestoreApplyDryRunOperation>,
next_sequence: &mut usize,
operation: RestoreApplyOperationKind,
members: impl IntoIterator<Item = &'a RestorePlanMember>,
) {
for member in members {
push_member_operation(operations, next_sequence, operation.clone(), member, None);
}
}
fn append_member_verification_operations(
operations: &mut Vec<RestoreApplyDryRunOperation>,
next_sequence: &mut usize,
members: &[&RestorePlanMember],
) {
for member in members {
for check in &member.verification_checks {
push_member_operation(
operations,
next_sequence,
RestoreApplyOperationKind::VerifyMember,
member,
Some(check),
);
}
}
}
fn push_member_operation(
operations: &mut Vec<RestoreApplyDryRunOperation>,
next_sequence: &mut usize,
operation: RestoreApplyOperationKind,
member: &RestorePlanMember,
check: Option<&VerificationCheck>,
) {
let sequence = *next_sequence;
*next_sequence += 1;
let snapshot_id = operation_snapshot_id(&operation, member);
let artifact_path = operation_artifact_path(&operation, member);
let artifact_checksum = operation_artifact_checksum(&operation, member);
operations.push(RestoreApplyDryRunOperation {
sequence,
operation,
member_order: member.member_order,
source_canister: member.source_canister.clone(),
target_canister: member.target_canister.clone(),
role: member.role.clone(),
snapshot_id,
artifact_path,
artifact_checksum,
verification_kind: check.map(|check| check.kind.clone()),
});
}
fn operation_snapshot_id(
operation: &RestoreApplyOperationKind,
member: &RestorePlanMember,
) -> Option<String> {
matches!(
operation,
RestoreApplyOperationKind::UploadSnapshot | RestoreApplyOperationKind::LoadSnapshot
)
.then(|| member.source_snapshot.snapshot_id.clone())
}
fn operation_artifact_path(
operation: &RestoreApplyOperationKind,
member: &RestorePlanMember,
) -> Option<String> {
matches!(
operation,
RestoreApplyOperationKind::UploadSnapshot | RestoreApplyOperationKind::LoadSnapshot
)
.then(|| member.source_snapshot.artifact_path.clone())
}
fn operation_artifact_checksum(
operation: &RestoreApplyOperationKind,
member: &RestorePlanMember,
) -> Option<ArtifactChecksum> {
matches!(
operation,
RestoreApplyOperationKind::UploadSnapshot | RestoreApplyOperationKind::LoadSnapshot
)
.then(|| {
member
.source_snapshot
.checksum
.as_ref()
.map(|hash| ArtifactChecksum {
algorithm: member.source_snapshot.checksum_algorithm.clone(),
hash: hash.clone(),
})
})
.flatten()
}
fn append_deployment_verification_operations(
plan: &RestorePlan,
operations: &mut Vec<RestoreApplyDryRunOperation>,
next_sequence: &mut usize,
) {
if plan.deployment_verification_checks.is_empty() {
return;
}
let root = plan
.members
.iter()
.find(|member| member.source_canister == plan.source_root_canister);
let source_canister = root.map_or_else(
|| plan.source_root_canister.clone(),
|member| member.source_canister.clone(),
);
let target_canister = root.map_or_else(
|| plan.source_root_canister.clone(),
|member| member.target_canister.clone(),
);
for check in &plan.deployment_verification_checks {
push_deployment_operation(
operations,
next_sequence,
&source_canister,
&target_canister,
check,
);
}
}
fn push_deployment_operation(
operations: &mut Vec<RestoreApplyDryRunOperation>,
next_sequence: &mut usize,
source_canister: &str,
target_canister: &str,
check: &VerificationCheck,
) {
let sequence = *next_sequence;
*next_sequence += 1;
let member_order = operations.len();
operations.push(RestoreApplyDryRunOperation {
sequence,
operation: RestoreApplyOperationKind::VerifyDeployment,
member_order,
source_canister: source_canister.to_string(),
target_canister: target_canister.to_string(),
role: "deployment".to_string(),
snapshot_id: None,
artifact_path: None,
artifact_checksum: None,
verification_kind: Some(check.kind.clone()),
});
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RestoreApplyDryRunOperation {
pub sequence: usize,
pub operation: RestoreApplyOperationKind,
pub member_order: usize,
pub source_canister: String,
pub target_canister: String,
pub role: String,
pub snapshot_id: Option<String>,
pub artifact_path: Option<String>,
pub artifact_checksum: Option<ArtifactChecksum>,
pub verification_kind: Option<String>,
}
#[derive(Debug, ThisError)]
pub enum RestoreApplyDryRunError {
#[error(transparent)]
InvalidPlan(#[from] RestorePlanError),
#[error("restore artifact path for {source_canister} escapes backup root: {artifact_path}")]
ArtifactPathEscapesBackup {
source_canister: String,
artifact_path: String,
},
#[error("restore backup artifact root IO failed at {backup_root}")]
ArtifactRootIo {
backup_root: PathBuf,
#[source]
source: std::io::Error,
},
#[error("restore backup artifact root is not a direct directory: {backup_root}")]
ArtifactRootUnsafe { backup_root: PathBuf },
#[error(
"restore artifact for {source_canister} has unsupported filesystem type {kind}: {artifact_path}"
)]
ArtifactUnsafeType {
source_canister: String,
artifact_path: String,
kind: String,
},
#[error(
"restore artifact for {source_canister} is missing: {artifact_path} at {resolved_path}"
)]
ArtifactMissing {
source_canister: String,
artifact_path: String,
resolved_path: String,
},
#[error("restore artifact checksum failed for {source_canister} at {artifact_path}: {source}")]
ArtifactChecksum {
source_canister: String,
artifact_path: String,
#[source]
source: ArtifactChecksumError,
},
}