use crate::fleet_ensure::{
generate::infrastructure_bootstrap::seed_projection,
model::{
DesiredFleet, FleetEnsurePlan,
infrastructure_bootstrap::{
InfrastructureBootstrapPublicationRecord, InfrastructureBootstrapRecord,
InfrastructureBootstrapSeedRecord,
},
},
ops,
ops::{
EnsurePaths,
infrastructure_bootstrap::{InfrastructureBootstrapError, seal_sources, terminal},
},
policy::expected_plan_sha256,
};
use candid::Principal;
use ic_host_fs::{
durable::write_bytes,
read::{read_file_no_follow, read_optional_file_no_follow},
};
use sha2_host::{Digest, Sha256};
use std::path::{Component, Path, PathBuf};
const MAX_BYTES: usize = 8 * 1024 * 1024;
const MAX_SEED_BYTES: usize = 1024 * 1024;
pub(in crate::fleet_ensure) fn bind(
paths: &EnsurePaths,
desired: &DesiredFleet,
source: &InfrastructureBootstrapRecord,
seed: &Path,
) -> Result<InfrastructureBootstrapRecord, InfrastructureBootstrapError> {
let workspace = paths.workspace.canonicalize()?;
let path = if seed.is_absolute() {
seed.to_owned()
} else {
workspace.join(seed)
};
let relative = path
.strip_prefix(&workspace)
.map_err(|_| invalid())?
.to_str()
.ok_or_else(invalid)?;
let path = target(paths, relative)?;
let bytes = read_file_no_follow(&path, MAX_SEED_BYTES).map_err(std::io::Error::from)?;
let original = String::from_utf8(bytes).map_err(|_| invalid())?;
seed_projection(desired, &original, None).map_err(|_| invalid())?;
let mut result = source.clone();
result.estate_seed = Some(InfrastructureBootstrapSeedRecord {
relative_path: relative.to_owned(),
before_sha256: hash(original.as_bytes()),
original,
});
seal_sources(result)
}
pub(in crate::fleet_ensure) fn validate_seed(
desired: &DesiredFleet,
source: &InfrastructureBootstrapRecord,
) -> Result<(), InfrastructureBootstrapError> {
if let Some(seed) = &source.estate_seed {
if seed.original.len() > MAX_SEED_BYTES
|| hash(seed.original.as_bytes()) != seed.before_sha256
{
return Err(invalid());
}
validate_path(&seed.relative_path)?;
seed_projection(desired, &seed.original, None).map_err(|_| invalid())?;
}
Ok(())
}
pub(in crate::fleet_ensure) fn validate_capacity(
plan: &FleetEnsurePlan,
) -> Result<(), InfrastructureBootstrapError> {
let desired = plan
.reviewed_desired
.as_ref()
.ok_or_else(invalid)?
.desired();
let Some(seed) = plan
.infrastructure_bootstrap
.as_ref()
.and_then(|source| source.estate_seed.as_ref())
else {
return Ok(());
};
let coordinator_name = &desired.bootstrap.as_ref().ok_or_else(invalid)?.coordinator;
let coordinator = desired
.canisters
.iter()
.find(|entry| entry.name == *coordinator_name)
.and_then(|entry| entry.principal.as_ref())
.map(|id| Principal::from_text(id).map_err(|_| invalid()))
.transpose()?
.unwrap_or_else(|| Principal::from_slice(&[255; 29]));
let replacement =
seed_projection(desired, &seed.original, Some(coordinator)).map_err(|_| invalid())?;
if replacement.len() > MAX_SEED_BYTES {
return Err(invalid());
}
let largest = InfrastructureBootstrapPublicationRecord {
schema_version: 1,
plan: plan.clone(),
replacement,
after_sha256: [255; 32],
terminal_receipt_sha256: [255; 32],
coordinator,
completed: false,
};
if serde_json::to_vec_pretty(&largest)?.len() > MAX_BYTES {
return Err(invalid());
}
Ok(())
}
fn path(paths: &EnsurePaths, digest: &str) -> Result<PathBuf, InfrastructureBootstrapError> {
if !ops::is_sha256(digest) {
return Err(invalid());
}
Ok(paths
.plan
.with_file_name("infrastructure-bootstrap-publications")
.join(format!("{digest}.json")))
}
pub(in crate::fleet_ensure) fn read(
paths: &EnsurePaths,
digest: &str,
) -> Result<Option<InfrastructureBootstrapPublicationRecord>, InfrastructureBootstrapError> {
let Some(bytes) =
read_optional_file_no_follow(&path(paths, digest)?, MAX_BYTES).map_err(|_| invalid())?
else {
return Ok(None);
};
let record: InfrastructureBootstrapPublicationRecord = serde_json::from_slice(&bytes)?;
let desired = record
.plan
.reviewed_desired
.as_ref()
.ok_or_else(invalid)?
.desired();
let source = record
.plan
.infrastructure_bootstrap
.as_ref()
.ok_or_else(invalid)?;
let seed = source.estate_seed.as_ref().ok_or_else(invalid)?;
validate_seed(desired, source)?;
let receipt_bytes = read_file_no_follow(&terminal::path(paths, &record.plan), MAX_BYTES)
.map_err(std::io::Error::from)?;
let receipt: crate::fleet_ensure::model::infrastructure_bootstrap::InfrastructureBootstrapTerminalRecord = serde_json::from_slice(&receipt_bytes)?;
let coordinator = &desired.bootstrap.as_ref().ok_or_else(invalid)?.coordinator;
if record.schema_version != 1
|| record.plan.plan_sha256 != digest
|| record.plan.plan_sha256 != expected_plan_sha256(&record.plan)
|| hash(record.replacement.as_bytes()) != record.after_sha256
|| seed_projection(desired, &seed.original, Some(record.coordinator))
.map_err(|_| invalid())?
!= record.replacement
|| hash(&receipt_bytes) != record.terminal_receipt_sha256
|| receipt.plan_sha256 != digest
|| receipt
.canisters
.get(coordinator)
.is_none_or(|sample| sample.binding.canister_id != record.coordinator)
{
return Err(invalid());
}
Ok(Some(record))
}
pub(in crate::fleet_ensure) fn publish(
paths: &EnsurePaths,
plan: &FleetEnsurePlan,
) -> Result<InfrastructureBootstrapPublicationRecord, InfrastructureBootstrapError> {
let _lock = ops::lock_operation(paths)?;
if let Some(record) = read(paths, &plan.plan_sha256)? {
if record.plan != *plan {
return Err(invalid());
}
return finish(paths, record);
}
let state = ops::read_state(paths, &plan.fleet)?;
let journal = ops::read_journal(paths)?.ok_or_else(invalid)?;
terminal::read_receipt(paths, plan, &journal, &state)?.ok_or_else(invalid)?;
let desired = plan
.reviewed_desired
.as_ref()
.ok_or_else(invalid)?
.desired();
let bootstrap = desired.bootstrap.as_ref().ok_or_else(invalid)?;
let seed = plan
.infrastructure_bootstrap
.as_ref()
.and_then(|source| source.estate_seed.as_ref())
.ok_or_else(invalid)?;
let coordinator = state
.principals
.get(&bootstrap.coordinator)
.and_then(|id| Principal::from_text(id).ok())
.ok_or_else(invalid)?;
let replacement =
seed_projection(desired, &seed.original, Some(coordinator)).map_err(|_| invalid())?;
let record = InfrastructureBootstrapPublicationRecord {
schema_version: 1,
plan: plan.clone(),
after_sha256: hash(replacement.as_bytes()),
replacement,
terminal_receipt_sha256: hash(
&read_file_no_follow(&terminal::path(paths, plan), MAX_BYTES)
.map_err(std::io::Error::from)?,
),
coordinator,
completed: false,
};
save(paths, &record)?;
finish(paths, record)
}
fn finish(
paths: &EnsurePaths,
mut record: InfrastructureBootstrapPublicationRecord,
) -> Result<InfrastructureBootstrapPublicationRecord, InfrastructureBootstrapError> {
if record.completed {
return Ok(record);
}
let seed = record
.plan
.infrastructure_bootstrap
.as_ref()
.and_then(|source| source.estate_seed.as_ref())
.ok_or_else(invalid)?;
let destination = target(paths, &seed.relative_path)?;
let bytes = read_file_no_follow(&destination, MAX_SEED_BYTES).map_err(std::io::Error::from)?;
if hash(&bytes) != record.after_sha256 {
if bytes != seed.original.as_bytes() {
return Err(invalid());
}
write_bytes(&destination, record.replacement.as_bytes())
.map_err(crate::publication::ops::io_error)?;
}
record.completed = true;
save(paths, &record)?;
Ok(record)
}
fn save(
paths: &EnsurePaths,
record: &InfrastructureBootstrapPublicationRecord,
) -> Result<(), InfrastructureBootstrapError> {
let bytes = serde_json::to_vec_pretty(record)?;
if bytes.len() > MAX_BYTES {
return Err(invalid());
}
Ok(write_bytes(&path(paths, &record.plan.plan_sha256)?, &bytes)
.map_err(crate::publication::ops::io_error)?)
}
fn validate_path(relative: &str) -> Result<(), InfrastructureBootstrapError> {
if relative.is_empty()
|| !Path::new(relative)
.components()
.all(|part| matches!(part, Component::Normal(_)))
{
return Err(invalid());
}
Ok(())
}
fn target(paths: &EnsurePaths, relative: &str) -> Result<PathBuf, InfrastructureBootstrapError> {
validate_path(relative)?;
let path = paths.workspace.canonicalize()?.join(relative);
if path.canonicalize()? != path {
return Err(invalid());
}
Ok(path)
}
fn hash(bytes: &[u8]) -> [u8; 32] {
Sha256::digest(bytes).into()
}
const fn invalid() -> InfrastructureBootstrapError {
InfrastructureBootstrapError::Integrity
}
pub(in crate::fleet_ensure) fn bind_holds(
desired: &DesiredFleet,
source: &InfrastructureBootstrapRecord,
) -> Result<DesiredFleet, InfrastructureBootstrapError> {
let mut result = desired.clone();
let bootstrap = result.bootstrap.as_mut().ok_or_else(invalid)?;
for root in &mut bootstrap.roots {
let mut sources = root
.canister_pool_imports
.iter()
.map(|name| {
let configured = desired
.canisters
.iter()
.find(|entry| entry.name == *name)
.ok_or_else(invalid)?;
Principal::from_text(configured.principal.as_ref().ok_or_else(invalid)?)
.map_err(|_| invalid())
})
.collect::<Result<Vec<_>, _>>()?;
sources.sort_unstable();
if sources.is_empty()
|| sources.len() > canic_contracts::ids::MAX_FLEET_CAPACITY_IMPORT_SOURCES
|| sources.windows(2).any(|pair| pair[0] == pair[1])
{
return Err(invalid());
}
root.capacity_import_bootstrap = Some(
crate::fleet_ensure::model::capacity_import::CapacityImportBootstrapRecord {
review_sha256: source.source_sha256,
operator: source.operator,
sources,
},
);
}
Ok(result)
}