pub(in crate::fleet_ensure) mod allowance;
mod owner;
#[cfg(test)]
mod tests;
use crate::fleet_ensure::{
model::attempt_recovery::AttemptRecoveryReviewRecord,
ops::{EnsurePaths, capacity_import::journal::CapacityImportJournalError},
};
use ic_host_fs::durable::write_bytes;
use ic_host_fs::read::read_file_no_follow;
use canic_core::cdk::utils::hash::hex_bytes;
use sha2_host::{Digest, Sha256};
pub(in crate::fleet_ensure) fn review(
paths: &EnsurePaths,
environment: &str,
fleet: &str,
) -> Result<AttemptRecoveryReviewRecord, CapacityImportJournalError> {
let owners = owner::discover(paths)?;
if owners.is_empty() {
return Err(CapacityImportJournalError::RequestInvalid);
}
let mut review = AttemptRecoveryReviewRecord {
schema_version: 1,
environment: environment.into(),
fleet: fleet.into(),
owners,
review_sha256: [0; 32],
};
review.review_sha256 = digest(&review)?;
let path = review_path(paths, review.review_sha256);
let bytes = serde_json::to_vec_pretty(&review)?;
if bytes.len() > owner::MAX_BYTES {
return Err(CapacityImportJournalError::Integrity);
}
match ic_host_fs::read::read_optional_file_no_follow(&path, owner::MAX_BYTES)
.map_err(|_| CapacityImportJournalError::Integrity)?
{
Some(bytes) if serde_json::from_slice::<AttemptRecoveryReviewRecord>(&bytes)? != review => {
return Err(CapacityImportJournalError::Integrity);
}
Some(_) => {}
None => write_bytes(&path, &bytes)?,
}
Ok(review)
}
pub(in crate::fleet_ensure) fn apply(
paths: &EnsurePaths,
environment: &str,
fleet: &str,
approved: [u8; 32],
) -> Result<AttemptRecoveryReviewRecord, CapacityImportJournalError> {
let bytes = read_file_no_follow(&review_path(paths, approved), owner::MAX_BYTES)
.map_err(std::io::Error::from)?;
let review: AttemptRecoveryReviewRecord = serde_json::from_slice(&bytes)?;
let owners = review
.owners
.iter()
.map(|owner| &owner.relative_path)
.collect::<std::collections::BTreeSet<_>>();
if review.schema_version != 1
|| review.environment != environment
|| review.fleet != fleet
|| review.review_sha256 != approved
|| digest(&review)? != approved
|| review.owners.is_empty()
|| owners.len() != review.owners.len()
{
return Err(CapacityImportJournalError::Integrity);
}
let updates = review
.owners
.iter()
.map(|target| owner::prepare(paths, target))
.collect::<Result<Vec<_>, _>>()?;
for update in updates.into_iter().flatten() {
write_bytes(&update.0, &update.1)?;
}
Ok(review)
}
fn review_path(paths: &EnsurePaths, digest: [u8; 32]) -> std::path::PathBuf {
paths
.plan
.with_file_name("attempt-recovery-reviews")
.join(format!("{}.json", hex_bytes(digest)))
}
fn digest(review: &AttemptRecoveryReviewRecord) -> Result<[u8; 32], CapacityImportJournalError> {
let mut review = review.clone();
review.review_sha256 = [0; 32];
let mut hash = Sha256::new();
hash.update(b"canic:host-attempt-recovery-review:v1\0");
hash.update(serde_json::to_vec(&review)?);
Ok(hash.finalize().into())
}