#[cfg(test)]
mod tests;
use crate::{
fleet_catalog::{
CommittedFleetCatalog, FleetCatalogEntryV1, FleetCatalogError, commit_fleet_catalog_entry,
},
fleet_install_plan::FleetInstallPlan,
};
use std::path::Path;
use candid::Principal;
use canic_core::{
control_plane_support::{
config::ComponentTopology, error::InternalError, ops::fleet_registry::FleetRegistryOps,
},
dto::{
fleet_registry::{
FleetRegistrySnapshotResponse, FleetRegistryVersion, FleetSubnetRootEntry,
FleetSubnetRootStatus,
},
fleet_subnet_root::FleetSubnetRootCanisterSummary,
},
ids::{FleetCoordinatorBinding, FleetName, FleetRegistryAuthority},
};
use thiserror::Error as ThisError;
pub(super) struct TerminalFleetCatalogPublicationRequest<'a> {
pub project_root: &'a Path,
pub fleet_name: FleetName,
pub environment: &'a str,
pub deployed_at_unix_secs: u64,
pub fleet_install_plan: &'a FleetInstallPlan,
pub component_topology: &'a ComponentTopology,
pub coordinator: Principal,
pub registry: &'a FleetRegistrySnapshotResponse,
pub root_summaries: &'a [FleetSubnetRootCanisterSummary],
}
#[derive(Debug, ThisError)]
pub(super) enum TerminalFleetCatalogPublicationError {
#[error("terminal Fleet catalog authority differs from the immutable install plan")]
AuthorityMismatch,
#[error(transparent)]
Catalog(#[from] FleetCatalogError),
#[error("terminal Fleet catalog publication time must be positive")]
NonPositiveDeploymentTime,
#[error("terminal Fleet Registry evidence is not exact")]
RegistryEvidenceMismatch,
#[error("terminal Fleet Registry validation failed: {0}")]
RegistryValidation(#[source] InternalError),
#[error("terminal Fleet root count or immutable root plan differs from the Registry")]
RootSetMismatch,
#[error("terminal evidence for Fleet Subnet Root {root} is incomplete or contradictory")]
RootSummaryMismatch { root: Principal },
}
pub(super) fn publish_terminal_fleet_catalog(
request: TerminalFleetCatalogPublicationRequest<'_>,
) -> Result<CommittedFleetCatalog, TerminalFleetCatalogPublicationError> {
if request.deployed_at_unix_secs == 0 {
return Err(TerminalFleetCatalogPublicationError::NonPositiveDeploymentTime);
}
validate_terminal_fleet_registry(
request.fleet_install_plan,
request.component_topology,
request.coordinator,
request.registry,
)?;
validate_terminal_root_summaries(
request.fleet_install_plan,
request.registry,
request.root_summaries,
)?;
let entry = FleetCatalogEntryV1 {
canonical_network_id: request.fleet_install_plan.fleet.fleet.canonical_network_id,
fleet_id: request.fleet_install_plan.fleet.fleet.fleet_id,
fleet_name: request.fleet_name,
app: request.fleet_install_plan.fleet.app.clone(),
environment: request.environment.to_string(),
deployed_at_unix_secs: request.deployed_at_unix_secs,
coordinator_principal: request.coordinator.to_text(),
};
commit_fleet_catalog_entry(request.project_root, entry).map_err(Into::into)
}
pub(super) fn validate_terminal_fleet_registry(
fleet_install_plan: &FleetInstallPlan,
component_topology: &ComponentTopology,
coordinator: Principal,
registry: &FleetRegistrySnapshotResponse,
) -> Result<(), TerminalFleetCatalogPublicationError> {
let expected_authority = FleetRegistryAuthority {
binding: FleetCoordinatorBinding {
fleet: fleet_install_plan.fleet.clone(),
coordinator_subnet: fleet_install_plan.coordinator.coordinator_subnet,
coordinator,
},
epoch: 1,
};
if coordinator == Principal::anonymous()
|| coordinator == Principal::management_canister()
|| registry.registry.authority != expected_authority
{
return Err(TerminalFleetCatalogPublicationError::AuthorityMismatch);
}
FleetRegistryOps::validate(&expected_authority, component_topology, ®istry.registry)
.map_err(TerminalFleetCatalogPublicationError::RegistryValidation)?;
let manifest =
FleetRegistryOps::manifest(&expected_authority, component_topology, ®istry.registry)
.map_err(TerminalFleetCatalogPublicationError::RegistryValidation)?;
let version =
FleetRegistryOps::version(&expected_authority, component_topology, ®istry.registry)
.map_err(TerminalFleetCatalogPublicationError::RegistryValidation)?;
if registry.manifest != manifest || registry.version != version {
return Err(TerminalFleetCatalogPublicationError::RegistryEvidenceMismatch);
}
validate_terminal_root_set(fleet_install_plan, registry)
}
fn validate_terminal_root_set(
fleet_install_plan: &FleetInstallPlan,
registry: &FleetRegistrySnapshotResponse,
) -> Result<(), TerminalFleetCatalogPublicationError> {
let planned_roots = &fleet_install_plan.fleet_subnet_roots;
let registry_roots = ®istry.registry.fleet_subnet_roots;
if planned_roots.is_empty() || planned_roots.len() != registry_roots.len() {
return Err(TerminalFleetCatalogPublicationError::RootSetMismatch);
}
for (planned, registered) in planned_roots.iter().zip(registry_roots) {
if !root_matches_plan(registered, planned) {
return Err(TerminalFleetCatalogPublicationError::RootSetMismatch);
}
}
Ok(())
}
fn validate_terminal_root_summaries(
fleet_install_plan: &FleetInstallPlan,
registry: &FleetRegistrySnapshotResponse,
root_summaries: &[FleetSubnetRootCanisterSummary],
) -> Result<(), TerminalFleetCatalogPublicationError> {
let planned_roots = &fleet_install_plan.fleet_subnet_roots;
let registry_roots = ®istry.registry.fleet_subnet_roots;
if planned_roots.len() != registry_roots.len() || registry_roots.len() != root_summaries.len() {
return Err(TerminalFleetCatalogPublicationError::RootSetMismatch);
}
for (registered, summary) in registry_roots.iter().zip(root_summaries) {
validate_root_summary(registered, ®istry.version, summary)?;
}
Ok(())
}
fn root_matches_plan(
registered: &FleetSubnetRootEntry,
planned: &crate::fleet_install_plan::PlannedFleetSubnetRoot,
) -> bool {
let expected_release_set = planned.initial_release_set;
registered.placement_subnet == planned.placement_subnet
&& registered.component_admissions == planned.component_admissions
&& registered.component_topology_digest == planned.component_topology_digest
&& registered.active_release_set == expected_release_set
&& registered.limits == planned.limits
&& registered.status == FleetSubnetRootStatus::Active
}
fn validate_root_summary(
registered: &FleetSubnetRootEntry,
registry_version: &FleetRegistryVersion,
summary: &FleetSubnetRootCanisterSummary,
) -> Result<(), TerminalFleetCatalogPublicationError> {
let total = summary
.infrastructure_canisters
.checked_add(summary.component_canisters);
let managed = 1_u32.checked_add(summary.component_canisters);
if summary.fleet_registry != *registry_version
|| summary.placement_subnet != registered.placement_subnet
|| summary.fleet_subnet_root != registered.fleet_subnet_root
|| summary.status != FleetSubnetRootStatus::Active
|| summary.infrastructure_canisters != 2
|| total != Some(summary.total_canisters)
|| managed.is_none_or(|count| count > registered.limits.maximum_managed_canisters)
{
return Err(TerminalFleetCatalogPublicationError::RootSummaryMismatch {
root: registered.fleet_subnet_root,
});
}
Ok(())
}