canic_host/installed_fleet/
mod.rs1use 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#[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#[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42pub enum InstalledFleetSource {
43 LocalReplica,
44 IcpCli,
45}
46
47#[derive(Clone, Debug, Eq, PartialEq)]
52pub struct InstalledFleetRegistry {
53 pub root_canister_id: String,
54 pub entries: Vec<RegistryEntry>,
55}
56
57#[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#[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(®istry);
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 ®istry.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;