Skip to main content

canic_host/installed_fleet/
mod.rs

1use crate::{
2    fleet_catalog::{FleetCatalogEntryV1, FleetCatalogError, read_fleet_catalog_entry_from_root},
3    icp::{IcpCli, IcpCommandError, existing_local_canister_candid_path},
4    registry::{RegistryEntry, RegistryParseError},
5    replica_query::ReplicaQueryError,
6    subnet_registry::{SubnetRegistryQueryError, SubnetRegistryQuerySource, query_subnet_registry},
7};
8use std::{collections::BTreeMap, path::Path};
9use thiserror::Error as ThisError;
10
11const IC_REJECT_CODE_DESTINATION_INVALID: u64 = 3;
12
13///
14/// InstalledFleetRequest
15///
16
17#[derive(Clone, Debug, Eq, PartialEq)]
18pub struct InstalledFleetRequest {
19    pub fleet: String,
20    pub environment: String,
21    pub icp: String,
22    pub detect_lost_local_root: bool,
23}
24
25///
26/// InstalledFleetResolution
27///
28
29#[derive(Clone, Debug, Eq, PartialEq)]
30pub struct InstalledFleetResolution {
31    pub source: InstalledFleetSource,
32    pub fleet: FleetCatalogEntryV1,
33    pub registry: InstalledFleetRegistry,
34    pub topology: ResolvedFleetTopology,
35}
36
37///
38/// InstalledFleetSource
39///
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42pub enum InstalledFleetSource {
43    LocalReplica,
44    IcpCli,
45}
46
47///
48/// InstalledFleetRegistry
49///
50
51#[derive(Clone, Debug, Eq, PartialEq)]
52pub struct InstalledFleetRegistry {
53    pub root_canister_id: String,
54    pub entries: Vec<RegistryEntry>,
55}
56
57///
58/// ResolvedFleetTopology
59///
60
61#[derive(Clone, Debug, Eq, PartialEq)]
62pub struct ResolvedFleetTopology {
63    pub root_canister_id: String,
64    pub children_by_parent: BTreeMap<Option<String>, Vec<String>>,
65    pub roles_by_canister: BTreeMap<String, String>,
66}
67
68///
69/// InstalledFleetError
70///
71
72#[derive(Debug, ThisError)]
73pub enum InstalledFleetError {
74    #[error("Fleet {fleet} is not installed on environment {environment}")]
75    NoInstalledFleet { environment: String, fleet: String },
76
77    #[error("failed to read the canonical-network Fleet catalog: {0}")]
78    FleetCatalog(#[from] FleetCatalogError),
79
80    #[error("local replica query failed: {0}")]
81    ReplicaQuery(#[source] ReplicaQueryError),
82
83    #[error(transparent)]
84    Icp(#[from] IcpCommandError),
85
86    #[error(
87        "Fleet {fleet} points to root {root}, but that canister is not present on environment {environment}"
88    )]
89    LostLocalFleet {
90        fleet: String,
91        environment: String,
92        root: String,
93    },
94
95    #[error(transparent)]
96    Registry(#[from] RegistryParseError),
97
98    #[error(transparent)]
99    Io(#[from] std::io::Error),
100}
101
102pub fn resolve_installed_fleet_from_root(
103    request: &InstalledFleetRequest,
104    icp_root: &Path,
105) -> Result<InstalledFleetResolution, InstalledFleetError> {
106    let fleet = read_installed_fleet_from_root(&request.environment, &request.fleet, icp_root)?;
107    let (source, entries) = query_registry_from_root(request, &fleet.root_principal, icp_root)?;
108    Ok(installed_fleet_resolution(fleet, source, entries))
109}
110
111fn installed_fleet_resolution(
112    fleet: FleetCatalogEntryV1,
113    source: InstalledFleetSource,
114    entries: Vec<RegistryEntry>,
115) -> InstalledFleetResolution {
116    let registry = InstalledFleetRegistry {
117        root_canister_id: fleet.root_principal.clone(),
118        entries,
119    };
120    let topology = ResolvedFleetTopology::from_registry(&registry);
121    InstalledFleetResolution {
122        source,
123        fleet,
124        registry,
125        topology,
126    }
127}
128
129pub fn read_installed_fleet_from_root(
130    environment: &str,
131    fleet: &str,
132    icp_root: &Path,
133) -> Result<FleetCatalogEntryV1, InstalledFleetError> {
134    read_fleet_catalog_entry_from_root(icp_root, environment, fleet)
135        .map_err(InstalledFleetError::FleetCatalog)?
136        .ok_or_else(|| InstalledFleetError::NoInstalledFleet {
137            environment: environment.to_string(),
138            fleet: fleet.to_string(),
139        })
140}
141
142impl ResolvedFleetTopology {
143    fn from_registry(registry: &InstalledFleetRegistry) -> Self {
144        let mut children_by_parent = BTreeMap::<Option<String>, Vec<String>>::new();
145        let mut roles_by_canister = BTreeMap::new();
146        for entry in &registry.entries {
147            children_by_parent
148                .entry(entry.parent_pid.clone())
149                .or_default()
150                .push(entry.pid.clone());
151            if let Some(role) = &entry.role {
152                roles_by_canister.insert(entry.pid.clone(), role.clone());
153            }
154        }
155        for children in children_by_parent.values_mut() {
156            children.sort();
157        }
158        Self {
159            root_canister_id: registry.root_canister_id.clone(),
160            children_by_parent,
161            roles_by_canister,
162        }
163    }
164}
165
166fn query_registry_from_root(
167    request: &InstalledFleetRequest,
168    root: &str,
169    icp_root: &Path,
170) -> Result<(InstalledFleetSource, Vec<RegistryEntry>), InstalledFleetError> {
171    let icp = IcpCli::new(&request.icp, Some(request.environment.clone())).with_cwd(icp_root);
172    let candid_path = existing_local_canister_candid_path(icp_root, &request.environment, "root");
173    let query = query_subnet_registry(
174        &icp,
175        root,
176        &request.environment,
177        Some(icp_root),
178        candid_path.as_deref(),
179    )
180    .map_err(|err| installed_fleet_registry_error(request, root, err))?;
181    Ok((installed_fleet_source(query.source), query.entries))
182}
183
184const fn installed_fleet_source(source: SubnetRegistryQuerySource) -> InstalledFleetSource {
185    match source {
186        SubnetRegistryQuerySource::LocalReplica => InstalledFleetSource::LocalReplica,
187        SubnetRegistryQuerySource::IcpCli => InstalledFleetSource::IcpCli,
188    }
189}
190
191fn installed_fleet_registry_error(
192    request: &InstalledFleetRequest,
193    root: &str,
194    error: SubnetRegistryQueryError,
195) -> InstalledFleetError {
196    match error {
197        SubnetRegistryQueryError::Replica(err) => local_registry_error(request, root, err),
198        SubnetRegistryQueryError::Icp(err) => InstalledFleetError::Icp(err),
199        SubnetRegistryQueryError::Registry(err) => InstalledFleetError::Registry(err),
200    }
201}
202
203fn local_registry_error(
204    request: &InstalledFleetRequest,
205    root: &str,
206    error: ReplicaQueryError,
207) -> InstalledFleetError {
208    if request.detect_lost_local_root && is_missing_destination_error(&error) {
209        return InstalledFleetError::LostLocalFleet {
210            fleet: request.fleet.clone(),
211            environment: request.environment.clone(),
212            root: root.to_string(),
213        };
214    }
215    InstalledFleetError::ReplicaQuery(error)
216}
217
218const fn is_missing_destination_error(error: &ReplicaQueryError) -> bool {
219    matches!(
220        error,
221        ReplicaQueryError::Rejected {
222            code: IC_REJECT_CODE_DESTINATION_INVALID,
223            ..
224        }
225    )
226}
227
228#[cfg(test)]
229mod tests;