#[cfg(test)]
mod tests;
use super::{
fleet_component_provisioning_plan::{
CompileFleetComponentProvisioningPlanRequest, compile_fleet_component_provisioning_plan,
},
fleet_registry_activation_journal::{
FleetRegistryActivationPhase, PlanFleetRegistryActivationRequest,
ResolvedFleetRegistryActivation, begin_registry_activation, plan_fleet_registry_activation,
record_registry_activated, record_registry_activation_verified,
},
fleet_registry_recovery::{
ActiveRegistryRecoveryRequest, require_active_or_service_successor_registry,
},
fleet_subnet_root_install_journal::{
FleetSubnetRootInstallPhase, PlanFleetSubnetRootInstallRequest,
expected_registry_join_entry, plan_fleet_subnet_root_install,
},
icp_context::InstallIcpContext,
operations::{
LiveRegistryEvidence, call_with_arg, fleet_registry_authority, query_live_registry,
query_with_arg, resolve_install_protocol_binding,
},
};
use crate::{
fleet_install_plan::PersistedFleetInstallPlan,
icp::IcpCli,
release_set::{
AppConfigSnapshot, CanicInfrastructureRole,
load_persisted_canic_infrastructure_artifact_manifest,
},
};
use std::path::Path;
use candid::Principal;
use canic_control_plane::dto::fleet_coordinator::{
CoordinatorCommand, CoordinatorCommandResponse, CoordinatorStatusRequest,
CoordinatorStatusResponse,
};
use canic_core::{
control_plane_support::{config::ComponentTopology, ops::fleet_registry::FleetRegistryOps},
dto::fleet_registry::{
FleetRegistry, FleetRegistryManifest, FleetRegistryVersion,
FleetSubnetRootSnapshotAcknowledgement,
},
protocol,
};
use thiserror::Error as ThisError;
const MAX_ACTIVATION_TRANSITIONS: usize = 4;
#[derive(Debug, ThisError)]
enum FleetRegistryActivationError {
#[error("root Registry activation requires RegistrySyncVerified, observed {0:?}")]
RootNotSynchronized(FleetSubnetRootInstallPhase),
#[error("planned all-Joining Registry differs from the verified synchronization version")]
JoiningVersionMismatch,
#[error("live Fleet Registry differs from the exact planned {0} snapshot")]
LiveRegistryMismatch(&'static str),
#[error("Coordinator acknowledgement set differs from the complete planned root set")]
AcknowledgementSetMismatch,
#[error("Fleet Registry activation exceeded its bounded journal transitions")]
TransitionBoundExceeded,
}
pub(super) struct VerifiedFleetRegistryActivation {
pub registry: FleetRegistry,
pub version: FleetRegistryVersion,
}
pub(super) struct ActivateFleetRegistryRequest<'a> {
pub icp: &'a InstallIcpContext,
pub config_path: &'a Path,
pub fleet_install_plan: &'a PersistedFleetInstallPlan,
pub coordinator: Principal,
pub install_operation_id: [u8; 32],
pub joining_version: FleetRegistryVersion,
}
#[expect(
clippy::too_many_lines,
reason = "one closed host sequence compiles, drives and independently verifies Registry activation"
)]
pub(super) fn activate_and_verify_fleet_registry(
request: ActivateFleetRegistryRequest<'_>,
) -> Result<VerifiedFleetRegistryActivation, Box<dyn std::error::Error>> {
let config = AppConfigSnapshot::load(request.config_path)?;
let component_topology = config.model().compile_component_topology()?;
let infrastructure_manifest = load_persisted_canic_infrastructure_artifact_manifest(
request.icp.root(),
request.fleet_install_plan.plan.release_build_id,
)?;
let authority = fleet_registry_authority(request.fleet_install_plan, request.coordinator);
let protocol_binding = resolve_install_protocol_binding(
request.icp,
&infrastructure_manifest,
CanicInfrastructureRole::FleetCoordinator,
)?;
let mut joining_registry = FleetRegistryOps::compile_genesis(
&request.fleet_install_plan.plan.fleet.app,
authority.clone(),
&component_topology,
request.fleet_install_plan.plan.admission.clone(),
)?;
let mut expected_roots =
Vec::with_capacity(request.fleet_install_plan.plan.fleet_subnet_roots.len());
for root_plan in &request.fleet_install_plan.plan.fleet_subnet_roots {
let current = plan_fleet_subnet_root_install(PlanFleetSubnetRootInstallRequest {
fleet_install_plan: request.fleet_install_plan,
infrastructure_manifest: &infrastructure_manifest,
coordinator: request.coordinator,
install_operation_id: request.install_operation_id,
component_topology: component_topology.clone(),
root_plan,
})?;
if !matches!(
current.journal.phase,
FleetSubnetRootInstallPhase::RegistrySyncVerified
| FleetSubnetRootInstallPhase::RegistryMirrorActivationInFlight
| FleetSubnetRootInstallPhase::RegistryMirrorActivated
| FleetSubnetRootInstallPhase::RegistryMirrorActivationVerified
| FleetSubnetRootInstallPhase::ComponentRegistryPreparationInFlight
| FleetSubnetRootInstallPhase::ComponentRegistryPrepared
| FleetSubnetRootInstallPhase::ComponentRegistryPreparationVerified
) {
return Err(
FleetRegistryActivationError::RootNotSynchronized(current.journal.phase).into(),
);
}
if current
.journal
.registry_sync_request
.as_ref()
.is_none_or(|sync| sync.expected_registry != request.joining_version)
{
return Err(FleetRegistryActivationError::JoiningVersionMismatch.into());
}
let entry = expected_registry_join_entry(¤t.journal)?;
expected_roots.push(entry.fleet_subnet_root);
joining_registry = FleetRegistryOps::compile_joining(
&authority,
&component_topology,
&joining_registry,
entry,
)?;
}
let planned_joining_version =
FleetRegistryOps::version(&authority, &component_topology, &joining_registry)?;
if planned_joining_version != request.joining_version {
return Err(FleetRegistryActivationError::JoiningVersionMismatch.into());
}
let planned = plan_fleet_registry_activation(PlanFleetRegistryActivationRequest {
fleet_install_plan: request.fleet_install_plan,
component_topology: component_topology.clone(),
joining_registry,
})?;
let icp = request.icp.cli();
let current = drive_activation(
icp,
&protocol_binding,
request.coordinator,
&component_topology,
expected_roots,
planned,
)?;
let live = query_live_registry(icp, &protocol_binding, request.coordinator)?;
let compiled =
compile_fleet_component_provisioning_plan(CompileFleetComponentProvisioningPlanRequest {
config: config.model(),
fleet_install_plan: &request.fleet_install_plan.plan,
registry: ¤t.journal.active_registry,
operation_id: super::root_component_provisioning_operation_id(
request.install_operation_id,
),
})?;
require_active_or_service_successor_registry(ActiveRegistryRecoveryRequest {
icp,
binding: &protocol_binding,
coordinator: request.coordinator,
component_topology: &component_topology,
active: ¤t.journal.active_registry,
live: &live,
expected_operation_id: compiled.prepare_request.operation_id,
expected_plan_hash: compiled.plan_hash,
})?;
let version = FleetRegistryOps::version(
¤t.journal.active_registry.authority,
&component_topology,
¤t.journal.active_registry,
)?;
Ok(VerifiedFleetRegistryActivation {
registry: current.journal.active_registry,
version,
})
}
fn drive_activation(
icp: &IcpCli,
binding: &crate::protocol_binding::ResolvedProtocolBinding,
coordinator: Principal,
component_topology: &ComponentTopology,
mut expected_roots: Vec<Principal>,
mut current: ResolvedFleetRegistryActivation,
) -> Result<ResolvedFleetRegistryActivation, Box<dyn std::error::Error>> {
for _ in 0..MAX_ACTIVATION_TRANSITIONS {
current = match current.journal.phase {
FleetRegistryActivationPhase::Planned => {
let live = query_live_registry(icp, binding, coordinator)?;
require_exact_registry(
component_topology,
¤t.journal.joining_registry,
&live,
"pre-activation all-Joining",
)?;
require_exact_acknowledgements(
icp,
binding,
coordinator,
&mut expected_roots,
¤t.journal.request.expected_registry,
)?;
begin_registry_activation(¤t)?
}
FleetRegistryActivationPhase::ActivationInFlight => {
let response: CoordinatorCommandResponse = call_with_arg(
icp,
binding,
coordinator,
protocol::CANIC_COMMAND,
&CoordinatorCommand::ActivateRegistry(current.journal.request.clone()),
)?;
let CoordinatorCommandResponse::ActivateRegistry(response) = response else {
return Err(FleetRegistryActivationError::LiveRegistryMismatch(
"activation response correlation",
)
.into());
};
record_registry_activated(¤t, response)?
}
FleetRegistryActivationPhase::Activated => {
let live = query_live_registry(icp, binding, coordinator)?;
require_exact_registry(
component_topology,
¤t.journal.active_registry,
&live,
"post-activation all-Active",
)?;
record_registry_activation_verified(¤t, live.manifest, live.version)?
}
FleetRegistryActivationPhase::Verified => return Ok(current),
};
}
Err(FleetRegistryActivationError::TransitionBoundExceeded.into())
}
fn require_exact_acknowledgements(
icp: &IcpCli,
binding: &crate::protocol_binding::ResolvedProtocolBinding,
coordinator: Principal,
expected_roots: &mut [Principal],
version: &FleetRegistryVersion,
) -> Result<(), Box<dyn std::error::Error>> {
let live = query_with_arg::<_, CoordinatorStatusResponse>(
icp,
binding,
coordinator,
protocol::CANIC_STATUS,
&CoordinatorStatusRequest::RootAcknowledgements,
)?;
let CoordinatorStatusResponse::RootAcknowledgements(live) = live else {
return Err("Coordinator returned an unrelated root-acknowledgement response".into());
};
expected_roots.sort_by(|left, right| left.as_slice().cmp(right.as_slice()));
let cardinality_matches = live.len() == expected_roots.len();
let entries_match = live
.iter()
.zip(expected_roots)
.all(|(ack, root)| acknowledgement_matches(ack, *root, version));
let acknowledgement_set_is_exact = [cardinality_matches, entries_match]
.into_iter()
.all(std::convert::identity);
if !acknowledgement_set_is_exact {
return Err(FleetRegistryActivationError::AcknowledgementSetMismatch.into());
}
Ok(())
}
fn acknowledgement_matches(
acknowledgement: &FleetSubnetRootSnapshotAcknowledgement,
root: Principal,
version: &FleetRegistryVersion,
) -> bool {
let expected = AcknowledgementAuthority { root, version };
let observed = AcknowledgementAuthority {
root: acknowledgement.fleet_subnet_root,
version: &acknowledgement.version,
};
observed == expected
}
fn require_exact_registry(
component_topology: &ComponentTopology,
expected: &FleetRegistry,
live: &LiveRegistryEvidence,
stage: &'static str,
) -> Result<(), Box<dyn std::error::Error>> {
let manifest = FleetRegistryOps::manifest(&expected.authority, component_topology, expected)?;
let version = FleetRegistryOps::version(&expected.authority, component_topology, expected)?;
let expected_evidence = RegistryEvidence {
registry: expected,
manifest: &manifest,
version: &version,
};
if RegistryEvidence::from_live(live) != expected_evidence {
return Err(FleetRegistryActivationError::LiveRegistryMismatch(stage).into());
}
Ok(())
}
#[derive(Eq, PartialEq)]
struct RegistryEvidence<'a> {
registry: &'a FleetRegistry,
manifest: &'a FleetRegistryManifest,
version: &'a FleetRegistryVersion,
}
#[derive(Eq, PartialEq)]
struct AcknowledgementAuthority<'a> {
root: Principal,
version: &'a FleetRegistryVersion,
}
impl<'a> RegistryEvidence<'a> {
const fn from_live(live: &'a LiveRegistryEvidence) -> Self {
Self {
registry: &live.registry,
manifest: &live.manifest,
version: &live.version,
}
}
}