use crate::{
sha256_hex, ActivationReceipt, ArtifactAuthenticityVerifier, ArtifactDescriptor, ArtifactStore,
StagedArtifact, UpdateError, UpdateProvider, UpdateRequest, UpdateResult,
};
use semver::Version;
#[cfg(test)]
struct TestOnlyArtifactVerifier;
#[cfg(test)]
impl ArtifactAuthenticityVerifier for TestOnlyArtifactVerifier {
fn verify(&self, _artifact: &ArtifactDescriptor) -> UpdateResult<()> {
Ok(())
}
}
#[cfg(test)]
static TEST_ONLY_ARTIFACTS: TestOnlyArtifactVerifier = TestOnlyArtifactVerifier;
struct VerifiedArtifact {
descriptor: ArtifactDescriptor,
bytes: Vec<u8>,
}
struct ActivatedArtifact {
descriptor: ArtifactDescriptor,
receipt: ActivationReceipt,
}
pub trait ActivationHealthCheck: Send + Sync {
fn check(&self, artifact: &ArtifactDescriptor) -> UpdateResult<()>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UpdateFaultPoint {
AfterSelection,
AfterVerification,
AfterStaging,
AfterActivation,
BeforeCommit,
}
pub trait UpdateFaultInjector: Send + Sync {
fn check(&self, point: UpdateFaultPoint) -> UpdateResult<()>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoUpdateFaults;
impl UpdateFaultInjector for NoUpdateFaults {
fn check(&self, _point: UpdateFaultPoint) -> UpdateResult<()> {
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UpdateOutcome {
NoUpdate,
Applied(ArtifactDescriptor),
RolledBack {
attempted: ArtifactDescriptor,
reason: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UpdatePreparation {
NoUpdate,
AwaitingHealth(Box<ArtifactDescriptor>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UpdateStaging {
NoUpdate,
Staged(Box<StagedArtifact>),
}
pub struct UpdateCoordinator<'a> {
provider: &'a dyn UpdateProvider,
store: &'a dyn ArtifactStore,
health: Option<&'a dyn ActivationHealthCheck>,
authenticity: &'a dyn ArtifactAuthenticityVerifier,
max_artifact_bytes: usize,
}
impl<'a> UpdateCoordinator<'a> {
pub fn new(
provider: &'a dyn UpdateProvider,
store: &'a dyn ArtifactStore,
health: &'a dyn ActivationHealthCheck,
max_artifact_bytes: usize,
) -> UpdateResult<Self> {
#[cfg(test)]
{
Self::new_with_authenticity(
provider,
store,
health,
&TEST_ONLY_ARTIFACTS,
max_artifact_bytes,
)
}
#[cfg(not(test))]
{
let _ = (provider, store, health, max_artifact_bytes);
Err(UpdateError::Authenticity(
"an explicit artifact authenticity verifier is required".to_string(),
))
}
}
pub fn new_with_authenticity(
provider: &'a dyn UpdateProvider,
store: &'a dyn ArtifactStore,
health: &'a dyn ActivationHealthCheck,
authenticity: &'a dyn ArtifactAuthenticityVerifier,
max_artifact_bytes: usize,
) -> UpdateResult<Self> {
if max_artifact_bytes == 0 {
return Err(UpdateError::InvalidArtifact(
"max_artifact_bytes must be greater than zero".to_string(),
));
}
Ok(Self {
provider,
store,
health: Some(health),
authenticity,
max_artifact_bytes,
})
}
pub fn new_for_preparation(
provider: &'a dyn UpdateProvider,
store: &'a dyn ArtifactStore,
authenticity: &'a dyn ArtifactAuthenticityVerifier,
max_artifact_bytes: usize,
) -> UpdateResult<Self> {
if max_artifact_bytes == 0 {
return Err(UpdateError::InvalidArtifact(
"max_artifact_bytes must be greater than zero".to_string(),
));
}
Ok(Self {
provider,
store,
health: None,
authenticity,
max_artifact_bytes,
})
}
pub fn apply(
&self,
request: &UpdateRequest,
runtime_version: &str,
protocol_version: &str,
) -> UpdateResult<UpdateOutcome> {
self.apply_with_faults(request, runtime_version, protocol_version, &NoUpdateFaults)
}
pub fn prepare(
&self,
request: &UpdateRequest,
runtime_version: &str,
protocol_version: &str,
) -> UpdateResult<UpdatePreparation> {
match self.stage_candidate(request, runtime_version, protocol_version)? {
UpdateStaging::NoUpdate => Ok(UpdatePreparation::NoUpdate),
UpdateStaging::Staged(staged) => {
let receipt = self.store.activate(*staged)?;
Ok(UpdatePreparation::AwaitingHealth(Box::new(
receipt.activated,
)))
}
}
}
pub fn stage_candidate(
&self,
request: &UpdateRequest,
runtime_version: &str,
protocol_version: &str,
) -> UpdateResult<UpdateStaging> {
self.store.recover()?;
let Some(candidate) = self.select_candidate(request, runtime_version, protocol_version)?
else {
return Ok(UpdateStaging::NoUpdate);
};
let verified = self.fetch_verified(candidate)?;
let staged = self.store.stage(&verified.descriptor, &verified.bytes)?;
Ok(UpdateStaging::Staged(Box::new(staged)))
}
pub fn apply_with_faults(
&self,
request: &UpdateRequest,
runtime_version: &str,
protocol_version: &str,
faults: &dyn UpdateFaultInjector,
) -> UpdateResult<UpdateOutcome> {
self.store.recover()?;
let Some(candidate) = self.select_candidate(request, runtime_version, protocol_version)?
else {
return Ok(UpdateOutcome::NoUpdate);
};
faults.check(UpdateFaultPoint::AfterSelection)?;
let verified = self.fetch_verified(candidate)?;
faults.check(UpdateFaultPoint::AfterVerification)?;
let activated = self.stage_and_activate(verified, faults)?;
self.finish_activation(activated, faults)
}
fn select_candidate(
&self,
request: &UpdateRequest,
runtime_version: &str,
protocol_version: &str,
) -> UpdateResult<Option<ArtifactDescriptor>> {
let Some(candidate) = self.provider.latest(request)? else {
return Ok(None);
};
if candidate.application_id() != &request.application_id {
return Err(UpdateError::Incompatible(
"artifact application identity differs from the request".to_string(),
));
}
if candidate.channel() != request.channel {
return Err(UpdateError::Incompatible(
"artifact update channel differs from the request".to_string(),
));
}
self.ensure_upgrade(request, &candidate)?;
candidate.ensure_compatible(runtime_version, protocol_version)?;
Ok(Some(candidate))
}
fn ensure_upgrade(
&self,
request: &UpdateRequest,
candidate: &ArtifactDescriptor,
) -> UpdateResult<()> {
let installed = Version::parse(&request.current_version).map_err(|error| {
UpdateError::Incompatible(format!("invalid installed application version: {error}"))
})?;
let candidate_version =
Version::parse(candidate.application_version()).map_err(|error| {
UpdateError::InvalidArtifact(format!(
"invalid candidate application version: {error}"
))
})?;
if candidate_version <= installed {
return Err(UpdateError::Incompatible(format!(
"candidate version {candidate_version} does not advance installed version {installed}"
)));
}
let Some(active) = self.store.current()? else {
return Ok(());
};
if active.application_id() != candidate.application_id() {
return Err(UpdateError::Incompatible(
"active artifact application identity differs from the candidate".to_string(),
));
}
if active.build_id() == candidate.build_id() {
return Err(UpdateError::Incompatible(
"candidate reuses the active build identity".to_string(),
));
}
let active_version = Version::parse(active.application_version()).map_err(|error| {
UpdateError::Store(format!("active artifact version is invalid: {error}"))
})?;
if candidate_version <= active_version {
return Err(UpdateError::Incompatible(format!(
"candidate version {candidate_version} does not advance active version {active_version}"
)));
}
Ok(())
}
fn fetch_verified(&self, candidate: ArtifactDescriptor) -> UpdateResult<VerifiedArtifact> {
let declared_size =
usize::try_from(candidate.size_bytes()).map_err(|_| UpdateError::ArtifactTooLarge {
max_bytes: self.max_artifact_bytes,
})?;
if declared_size > self.max_artifact_bytes {
return Err(UpdateError::ArtifactTooLarge {
max_bytes: self.max_artifact_bytes,
});
}
let bytes = self.provider.fetch(&candidate, self.max_artifact_bytes)?;
if bytes.len() > self.max_artifact_bytes || bytes.len() != declared_size {
return Err(UpdateError::ArtifactTooLarge {
max_bytes: self.max_artifact_bytes,
});
}
if sha256_hex(&bytes) != candidate.sha256() {
return Err(UpdateError::ChecksumMismatch);
}
self.authenticity.verify(&candidate)?;
Ok(VerifiedArtifact {
descriptor: candidate,
bytes,
})
}
fn stage_and_activate(
&self,
verified: VerifiedArtifact,
faults: &dyn UpdateFaultInjector,
) -> UpdateResult<ActivatedArtifact> {
let staged = self.store.stage(&verified.descriptor, &verified.bytes)?;
faults.check(UpdateFaultPoint::AfterStaging)?;
let receipt = self.store.activate(staged)?;
Ok(ActivatedArtifact {
descriptor: verified.descriptor,
receipt,
})
}
fn finish_activation(
&self,
activated: ActivatedArtifact,
faults: &dyn UpdateFaultInjector,
) -> UpdateResult<UpdateOutcome> {
let health = self.health.ok_or_else(|| {
UpdateError::Health("activation health check is not configured".to_string())
})?;
let result = faults
.check(UpdateFaultPoint::AfterActivation)
.and_then(|_| health.check(&activated.descriptor))
.and_then(|_| faults.check(UpdateFaultPoint::BeforeCommit))
.and_then(|_| self.store.commit(&activated.receipt));
match result {
Ok(()) => Ok(UpdateOutcome::Applied(activated.descriptor)),
Err(cause) => self.rollback_after_failure(activated, cause),
}
}
fn rollback_after_failure(
&self,
activated: ActivatedArtifact,
cause: UpdateError,
) -> UpdateResult<UpdateOutcome> {
match self.store.rollback(&activated.receipt) {
Ok(()) => Ok(UpdateOutcome::RolledBack {
attempted: activated.descriptor,
reason: cause.to_string(),
}),
Err(rollback) => Err(UpdateError::RollbackFailed {
cause: cause.to_string(),
rollback: rollback.to_string(),
}),
}
}
}