use crate::{
ActivationReceiptV2, ArtifactDescriptor, RecoveryAction, RecoveryDecision, StagedArtifact,
UpdateError, UpdateResult,
};
const MAX_ATTEMPT_ID_BYTES: usize = 128;
const MAX_HOST_BINDING_BYTES: usize = 512;
#[derive(Clone, PartialEq, Eq)]
pub struct ActivationRequest {
pub attempt_id: String,
pub descriptor: ArtifactDescriptor,
pub previous: Option<ArtifactDescriptor>,
pub host_binding: Option<String>,
}
impl ActivationRequest {
pub fn validate(&self) -> UpdateResult<()> {
validate_text("attempt_id", &self.attempt_id, MAX_ATTEMPT_ID_BYTES)?;
self.descriptor.validate()?;
if let Some(previous) = &self.previous {
previous.validate()?;
}
if let Some(binding) = &self.host_binding {
validate_text("host_binding", binding, MAX_HOST_BINDING_BYTES)?;
}
Ok(())
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct ActivationEvidence {
pub attempt_id: String,
pub descriptor: ArtifactDescriptor,
pub host_binding: Option<String>,
}
impl ActivationEvidence {
pub fn validate(&self) -> UpdateResult<()> {
validate_text("attempt_id", &self.attempt_id, MAX_ATTEMPT_ID_BYTES)?;
self.descriptor.validate()?;
if let Some(binding) = &self.host_binding {
validate_text("host_binding", binding, MAX_HOST_BINDING_BYTES)?;
}
Ok(())
}
pub fn into_receipt(
self,
previous: Option<ArtifactDescriptor>,
created_at_ms: u64,
) -> UpdateResult<ActivationReceiptV2> {
self.validate()?;
if let Some(previous) = &previous {
previous.validate()?;
}
Ok(ActivationReceiptV2 {
format_version: crate::ACTIVATION_V2_FORMAT_VERSION,
attempt_id: self.attempt_id,
phase: crate::ActivationPhaseV2::ActivatedPendingHealth,
activated: self.descriptor,
previous,
created_at_ms,
updated_at_ms: created_at_ms,
host_binding: self.host_binding,
failure_reason: None,
})
}
}
pub trait ActivationAdapter: Send + Sync {
fn prepare(&self, request: &ActivationRequest, staged: &StagedArtifact) -> UpdateResult<()>;
fn activate(
&self,
request: &ActivationRequest,
staged: &StagedArtifact,
) -> UpdateResult<ActivationEvidence>;
fn healthcheck(&self, evidence: &ActivationEvidence) -> UpdateResult<()>;
fn commit(&self, receipt: &ActivationReceiptV2) -> UpdateResult<()>;
fn rollback(&self, receipt: &ActivationReceiptV2) -> UpdateResult<()>;
fn recover(&self, decision: &RecoveryDecision) -> UpdateResult<RecoveryAction>;
}
fn validate_text(name: &str, value: &str, max: usize) -> UpdateResult<()> {
if value.trim().is_empty() || value.len() > max || value.chars().any(char::is_control) {
return Err(UpdateError::Recovery(format!(
"activation {name} is empty, too long or contains control characters"
)));
}
Ok(())
}
impl std::fmt::Debug for ActivationRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ActivationRequest")
.field("has_previous", &self.previous.is_some())
.field("has_host_binding", &self.host_binding.is_some())
.finish_non_exhaustive()
}
}
impl std::fmt::Debug for ActivationEvidence {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ActivationEvidence")
.field("has_host_binding", &self.host_binding.is_some())
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
use appcore_contracts::{ApplicationId, BuildId};
fn descriptor() -> ArtifactDescriptor {
ArtifactDescriptor::new(
ApplicationId::new("app-a").unwrap(),
"1.1.0",
BuildId::new("build-a").unwrap(),
"stable",
">=0.6.0, <1.0.0",
"1",
"memory:build-a",
"00".repeat(32),
1,
)
.unwrap()
}
#[test]
fn activation_evidence_becomes_pending_v2_receipt() {
let evidence = ActivationEvidence {
attempt_id: "attempt-a".to_string(),
descriptor: descriptor(),
host_binding: Some("host-a".to_string()),
};
let receipt = evidence.into_receipt(None, 100).unwrap();
assert_eq!(
receipt.phase,
crate::ActivationPhaseV2::ActivatedPendingHealth
);
assert_eq!(receipt.attempt_id, "attempt-a");
}
#[test]
fn activation_context_rejects_unbounded_identifiers() {
let request = ActivationRequest {
attempt_id: "bad\nattempt".to_string(),
descriptor: descriptor(),
previous: None,
host_binding: None,
};
assert!(matches!(
request.validate(),
Err(UpdateError::Recovery(message)) if message.contains("attempt_id")
));
}
}