1use crate::{
2 icp::{IcpCli, IcpCommandError},
3 install_root::{InstallState, read_named_fleet_install_state},
4 registry::{RegistryEntry, RegistryParseError, parse_registry_entries},
5 replica_query,
6};
7use std::collections::BTreeMap;
8use thiserror::Error as ThisError;
9
10#[derive(Clone, Debug, Eq, PartialEq)]
15pub struct InstalledFleetRequest {
16 pub fleet: String,
17 pub network: String,
18 pub icp: String,
19 pub detect_lost_local_root: bool,
20}
21
22#[derive(Clone, Debug, Eq, PartialEq)]
27pub struct InstalledFleetResolution {
28 pub source: InstalledFleetSource,
29 pub state: InstallState,
30 pub registry: InstalledFleetRegistry,
31 pub topology: ResolvedFleetTopology,
32}
33
34#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub enum InstalledFleetSource {
40 LocalReplica,
41 IcpCli,
42}
43
44#[derive(Clone, Debug, Eq, PartialEq)]
49pub struct InstalledFleetRegistry {
50 pub root_canister_id: String,
51 pub entries: Vec<RegistryEntry>,
52}
53
54#[derive(Clone, Debug, Eq, PartialEq)]
59pub struct ResolvedFleetTopology {
60 pub root_canister_id: String,
61 pub children_by_parent: BTreeMap<Option<String>, Vec<String>>,
62 pub roles_by_canister: BTreeMap<String, String>,
63}
64
65#[derive(Debug, ThisError)]
70pub enum InstalledFleetError {
71 #[error("fleet {fleet} is not installed on network {network}")]
72 NoInstalledFleet { network: String, fleet: String },
73
74 #[error("failed to read canic fleet state: {0}")]
75 InstallState(String),
76
77 #[error("local replica query failed: {0}")]
78 ReplicaQuery(String),
79
80 #[error("icp command failed: {command}\n{stderr}")]
81 IcpFailed { command: String, stderr: String },
82
83 #[error(
84 "fleet {fleet} points to root {root}, but that canister is not present on network {network}"
85 )]
86 LostLocalFleet {
87 fleet: String,
88 network: String,
89 root: String,
90 },
91
92 #[error(transparent)]
93 Registry(#[from] RegistryParseError),
94
95 #[error(transparent)]
96 Io(#[from] std::io::Error),
97}
98
99pub fn resolve_installed_fleet(
100 request: &InstalledFleetRequest,
101) -> Result<InstalledFleetResolution, InstalledFleetError> {
102 let state = read_installed_fleet_state(&request.network, &request.fleet)?;
103 let (source, registry_json) = query_registry(request, &state.root_canister_id)?;
104 let entries = parse_registry_entries(®istry_json)?;
105 let registry = InstalledFleetRegistry {
106 root_canister_id: state.root_canister_id.clone(),
107 entries,
108 };
109 let topology = ResolvedFleetTopology::from_registry(®istry);
110 Ok(InstalledFleetResolution {
111 source,
112 state,
113 registry,
114 topology,
115 })
116}
117
118pub fn read_installed_fleet_state(
119 network: &str,
120 fleet: &str,
121) -> Result<InstallState, InstalledFleetError> {
122 read_named_fleet_install_state(network, fleet)
123 .map_err(|err| InstalledFleetError::InstallState(err.to_string()))?
124 .ok_or_else(|| InstalledFleetError::NoInstalledFleet {
125 network: network.to_string(),
126 fleet: fleet.to_string(),
127 })
128}
129
130impl ResolvedFleetTopology {
131 fn from_registry(registry: &InstalledFleetRegistry) -> Self {
132 let mut children_by_parent = BTreeMap::<Option<String>, Vec<String>>::new();
133 let mut roles_by_canister = BTreeMap::new();
134 for entry in ®istry.entries {
135 children_by_parent
136 .entry(entry.parent_pid.clone())
137 .or_default()
138 .push(entry.pid.clone());
139 if let Some(role) = &entry.role {
140 roles_by_canister.insert(entry.pid.clone(), role.clone());
141 }
142 }
143 for children in children_by_parent.values_mut() {
144 children.sort();
145 }
146 Self {
147 root_canister_id: registry.root_canister_id.clone(),
148 children_by_parent,
149 roles_by_canister,
150 }
151 }
152}
153
154fn query_registry(
155 request: &InstalledFleetRequest,
156 root: &str,
157) -> Result<(InstalledFleetSource, String), InstalledFleetError> {
158 if replica_query::should_use_local_replica_query(Some(&request.network)) {
159 return replica_query::query_subnet_registry_json(Some(&request.network), root)
160 .map(|registry| (InstalledFleetSource::LocalReplica, registry))
161 .map_err(|err| local_registry_error(request, root, err.to_string()));
162 }
163
164 IcpCli::new(&request.icp, None, Some(request.network.clone()))
165 .canister_call_output(root, "canic_subnet_registry", Some("json"))
166 .map(|registry| (InstalledFleetSource::IcpCli, registry))
167 .map_err(installed_fleet_icp_error)
168}
169
170fn local_registry_error(
171 request: &InstalledFleetRequest,
172 root: &str,
173 error: String,
174) -> InstalledFleetError {
175 if request.detect_lost_local_root && is_canister_not_found_error(&error) {
176 return InstalledFleetError::LostLocalFleet {
177 fleet: request.fleet.clone(),
178 network: request.network.clone(),
179 root: root.to_string(),
180 };
181 }
182 InstalledFleetError::ReplicaQuery(error)
183}
184
185fn is_canister_not_found_error(error: &str) -> bool {
186 error.contains("Canister ") && error.contains(" not found")
187}
188
189fn installed_fleet_icp_error(error: IcpCommandError) -> InstalledFleetError {
190 match error {
191 IcpCommandError::Io(err) => InstalledFleetError::Io(err),
192 IcpCommandError::Failed { command, stderr } => {
193 InstalledFleetError::IcpFailed { command, stderr }
194 }
195 IcpCommandError::SnapshotIdUnavailable { output } => InstalledFleetError::IcpFailed {
196 command: "icp canister snapshot create".to_string(),
197 stderr: output,
198 },
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
208 fn topology_indexes_registry_entries() {
209 let registry = InstalledFleetRegistry {
210 root_canister_id: "root-id".to_string(),
211 entries: vec![
212 RegistryEntry {
213 pid: "child-b".to_string(),
214 role: Some("worker".to_string()),
215 kind: None,
216 parent_pid: Some("root-id".to_string()),
217 module_hash: None,
218 },
219 RegistryEntry {
220 pid: "root-id".to_string(),
221 role: Some("root".to_string()),
222 kind: None,
223 parent_pid: None,
224 module_hash: None,
225 },
226 RegistryEntry {
227 pid: "child-a".to_string(),
228 role: Some("app".to_string()),
229 kind: None,
230 parent_pid: Some("root-id".to_string()),
231 module_hash: None,
232 },
233 ],
234 };
235
236 let topology = ResolvedFleetTopology::from_registry(®istry);
237
238 assert_eq!(
239 topology
240 .children_by_parent
241 .get(&Some("root-id".to_string())),
242 Some(&vec!["child-a".to_string(), "child-b".to_string()])
243 );
244 assert_eq!(topology.roles_by_canister["child-a"], "app");
245 assert_eq!(topology.root_canister_id, "root-id");
246 }
247
248 #[test]
250 fn detects_local_canister_not_found_error() {
251 assert!(is_canister_not_found_error(
252 "local replica rejected query: code=3 message=Canister uxrrr-q7777-77774-qaaaq-cai not found"
253 ));
254 assert!(!is_canister_not_found_error(
255 "local replica rejected query: code=5 message=some other failure"
256 ));
257 }
258}