use crate::domain::policy::pure::fleet_admission_projection::{
FleetAdmissionTargetTransitionError, transition_fleet_admission_projection,
};
use crate::{
InternalError,
domain::policy::pure::{
fleet_admission::effective_fleet_admission_principals,
fleet_admission_projection::open_fresh_fleet_admission_projection,
},
dto::{
fleet_activation::FleetActivationPhase,
fleet_admission::{
FleetAdmissionActivateTargetRequest, FleetAdmissionOpenTargetRequest,
FleetAdmissionPrepareTargetRequest, FleetAdmissionPreparedProjectionStatus,
FleetAdmissionProjectionPhase, FleetAdmissionProjectionStatusResponse,
FleetAdmissionTargetReceipt,
},
page::{Page, PageRequest},
},
ids::{
FleetAdmissionPolicy, FleetAdmissionProjection, MAX_FLEET_ADMISSION_PROJECTION_PAGE,
ManagedCanisterBinding,
},
model::fleet_admission_projection::{
FleetAdmissionProjectionPhaseModel, FleetAdmissionProjectionState,
FleetAdmissionProjectionValidationError,
},
ops::{
fleet_admission_policy::{
fleet_admission_activate_target_request, fleet_admission_open_target_request,
fleet_admission_prepare_target_request, fleet_admission_target_for_binding,
fleet_admission_target_receipt, materialize_fleet_admission_projection,
},
ic::IcOps,
runtime::env::EnvOps,
storage::{
fleet_activation::FleetActivationOps,
fleet_admission_projection::FleetAdmissionProjectionOps,
},
},
};
pub fn compile_fleet_admission_projection(
policy: &FleetAdmissionPolicy,
target: ManagedCanisterBinding,
) -> Result<FleetAdmissionProjection, FleetAdmissionProjectionValidationError> {
let selector_target = fleet_admission_target_for_binding(&target);
let principals = effective_fleet_admission_principals(policy, &selector_target);
materialize_fleet_admission_projection(policy, target, principals)
}
pub struct FleetAdmissionProjectionWorkflow;
impl FleetAdmissionProjectionWorkflow {
pub(crate) fn initialize(projection: FleetAdmissionProjection) -> Result<(), InternalError> {
let expected = projection.target.clone();
validate_target_is_self(&expected)?;
FleetAdmissionProjectionOps::initialize(projection, &expected)
}
pub(crate) fn restore() -> Result<(), InternalError> {
let expected = exact_managed_target()?;
FleetAdmissionProjectionOps::validated(&expected).map(|_state| ())
}
pub(crate) fn open_fresh() -> Result<bool, InternalError> {
let activation = FleetActivationOps::status(false)
.map_err(crate::ops::storage::StorageOpsError::from)?;
if activation.phase != FleetActivationPhase::Active {
return Err(InternalError::conflict());
}
let expected = exact_managed_target()?;
let state = FleetAdmissionProjectionOps::validated(&expected)?;
let decision = open_fresh_fleet_admission_projection(&state)
.map_err(|_error| InternalError::invariant())?;
if decision.transitioned {
FleetAdmissionProjectionOps::replace(decision.state)?;
}
Ok(decision.transitioned)
}
pub(crate) fn contains(principal: candid::Principal) -> Result<bool, InternalError> {
let expected = exact_managed_target()?;
let state = FleetAdmissionProjectionOps::validated(&expected)?;
if state.phase != FleetAdmissionProjectionPhaseModel::Open {
return Ok(false);
}
Ok(state.active.principals.binary_search(&principal).is_ok())
}
pub(crate) fn prepare(
request: FleetAdmissionPrepareTargetRequest,
) -> Result<FleetAdmissionTargetReceipt, InternalError> {
let expected = exact_managed_target()?;
let state = FleetAdmissionProjectionOps::validated(&expected)?;
let request = fleet_admission_prepare_target_request(request, &expected)
.map_err(|_error| InternalError::invalid_input())?;
transition(state, request)
}
pub(crate) fn activate(
request: FleetAdmissionActivateTargetRequest,
) -> Result<FleetAdmissionTargetReceipt, InternalError> {
let expected = exact_managed_target()?;
let state = FleetAdmissionProjectionOps::validated(&expected)?;
let request = fleet_admission_activate_target_request(&state, request)
.map_err(|_error| InternalError::conflict())?;
transition(state, request)
}
pub(crate) fn open(
request: FleetAdmissionOpenTargetRequest,
) -> Result<FleetAdmissionTargetReceipt, InternalError> {
let expected = exact_managed_target()?;
let state = FleetAdmissionProjectionOps::validated(&expected)?;
let request = fleet_admission_open_target_request(&state, request)
.map_err(|_error| InternalError::conflict())?;
transition(state, request)
}
pub(crate) fn status(
request: PageRequest,
) -> Result<FleetAdmissionProjectionStatusResponse, InternalError> {
let expected = exact_managed_target()?;
let state = FleetAdmissionProjectionOps::validated(&expected)?;
status_from_state(state, request)
}
}
fn transition(
state: FleetAdmissionProjectionState,
request: crate::model::fleet_admission_projection::FleetAdmissionTargetTransitionRequestModel,
) -> Result<FleetAdmissionTargetReceipt, InternalError> {
let projection = request.successor.clone();
let decision =
transition_fleet_admission_projection(&state, request).map_err(map_transition_error)?;
if !decision.replayed {
FleetAdmissionProjectionOps::replace(decision.state)?;
}
Ok(fleet_admission_target_receipt(
&projection,
&decision.receipt,
))
}
const fn map_transition_error(error: FleetAdmissionTargetTransitionError) -> InternalError {
match error {
FleetAdmissionTargetTransitionError::EmptyOperationId => InternalError::invalid_input(),
FleetAdmissionTargetTransitionError::OperationConflict
| FleetAdmissionTargetTransitionError::PhaseConflict
| FleetAdmissionTargetTransitionError::SuccessorConflict => InternalError::conflict(),
FleetAdmissionTargetTransitionError::InvalidState => InternalError::invariant(),
}
}
fn status_from_state(
state: FleetAdmissionProjectionState,
request: PageRequest,
) -> Result<FleetAdmissionProjectionStatusResponse, InternalError> {
let total =
u64::try_from(state.active.principals.len()).map_err(|_| InternalError::invariant())?;
let limit = request.limit.min(MAX_FLEET_ADMISSION_PROJECTION_PAGE);
let entries = usize::try_from(request.offset)
.ok()
.filter(|offset| *offset < state.active.principals.len())
.map_or_else(Vec::new, |offset| {
let take = usize::try_from(limit).expect("projection page limit fits usize");
state
.active
.principals
.iter()
.skip(offset)
.take(take)
.copied()
.collect()
});
Ok(FleetAdmissionProjectionStatusResponse {
authority: state.active.authority,
target: state.active.target,
generation: state.active.generation,
policy_digest: state.active.policy_digest,
projection_digest: state.active.projection_digest,
phase: match state.phase {
FleetAdmissionProjectionPhaseModel::Fenced => FleetAdmissionProjectionPhase::Fenced,
FleetAdmissionProjectionPhaseModel::Open => FleetAdmissionProjectionPhase::Open,
},
prepared: state
.prepared
.map(|projection| FleetAdmissionPreparedProjectionStatus {
generation: projection.generation,
policy_digest: projection.policy_digest,
projection_digest: projection.projection_digest,
}),
principals: Page { entries, total },
maximum_page_size: u16::try_from(MAX_FLEET_ADMISSION_PROJECTION_PAGE)
.expect("projection page limit fits u16"),
})
}
fn exact_managed_target() -> Result<ManagedCanisterBinding, InternalError> {
let target = EnvOps::managed_binding()?;
validate_target_is_self(&target)?;
Ok(target)
}
fn validate_target_is_self(target: &ManagedCanisterBinding) -> Result<(), InternalError> {
let target_canister = match target {
ManagedCanisterBinding::Component(binding) => binding.canister_id,
ManagedCanisterBinding::ComponentChild(binding) => binding.canister_id,
};
if target_canister != IcOps::canister_self() {
return Err(InternalError::invariant());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn local_status_pages_are_canonical_clamped_and_bounded() {
let mut projection = crate::test::support::fleet_admission_projection(
crate::test::support::managed_component_binding(),
);
projection.principals = (0..=MAX_FLEET_ADMISSION_PROJECTION_PAGE)
.map(|index| candid::Principal::from_slice(&index.to_be_bytes()))
.collect();
let state = FleetAdmissionProjectionState {
schema_version: 1,
active: projection,
prepared: None,
phase: FleetAdmissionProjectionPhaseModel::Fenced,
last_receipt: None,
};
let first = status_from_state(
state.clone(),
PageRequest {
offset: 0,
limit: u64::MAX,
},
)
.expect("bounded first page");
assert_eq!(
first.principals.total,
MAX_FLEET_ADMISSION_PROJECTION_PAGE + 1
);
assert_eq!(first.principals.entries.len(), 128);
let empty = status_from_state(
state,
PageRequest {
offset: u64::MAX,
limit: 1,
},
)
.expect("empty out-of-range page");
assert!(empty.principals.entries.is_empty());
assert_eq!(
first.maximum_page_size,
u16::try_from(MAX_FLEET_ADMISSION_PROJECTION_PAGE).expect("bounded")
);
assert!(first.principals.total <= crate::ids::MAX_FLEET_ADMISSION_PRINCIPALS as u64);
}
}