1use crate::{
2 icp::{IcpCli, IcpCommandError, existing_local_canister_candid_path},
3 install_root::{
4 InstallState, read_named_deployment_install_state,
5 read_named_deployment_install_state_from_root,
6 },
7 registry::{RegistryEntry, RegistryParseError, parse_registry_entries},
8 replica_query,
9};
10use std::{collections::BTreeMap, path::Path};
11use thiserror::Error as ThisError;
12
13#[derive(Clone, Debug, Eq, PartialEq)]
18pub struct InstalledDeploymentRequest {
19 pub deployment: String,
20 pub network: String,
21 pub icp: String,
22 pub detect_lost_local_root: bool,
23}
24
25#[derive(Clone, Debug, Eq, PartialEq)]
30pub struct InstalledDeploymentResolution {
31 pub source: InstalledDeploymentSource,
32 pub state: InstallState,
33 pub registry: InstalledDeploymentRegistry,
34 pub topology: ResolvedDeploymentTopology,
35}
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42pub enum InstalledDeploymentSource {
43 LocalReplica,
44 IcpCli,
45}
46
47#[derive(Clone, Debug, Eq, PartialEq)]
52pub struct InstalledDeploymentRegistry {
53 pub root_canister_id: String,
54 pub entries: Vec<RegistryEntry>,
55}
56
57#[derive(Clone, Debug, Eq, PartialEq)]
62pub struct ResolvedDeploymentTopology {
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 InstalledDeploymentError {
74 #[error("deployment target {deployment} is not installed on network {network}")]
75 NoInstalledDeployment { network: String, deployment: String },
76
77 #[error("failed to read canic deployment state: {0}")]
78 InstallState(String),
79
80 #[error("local replica query failed: {0}")]
81 ReplicaQuery(String),
82
83 #[error("icp command failed: {command}\n{stderr}")]
84 IcpFailed { command: String, stderr: String },
85
86 #[error(
87 "deployment target {deployment} points to root {root}, but that canister is not present on network {network}"
88 )]
89 LostLocalDeployment {
90 deployment: String,
91 network: 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_deployment(
103 request: &InstalledDeploymentRequest,
104) -> Result<InstalledDeploymentResolution, InstalledDeploymentError> {
105 let state = read_installed_deployment_state(&request.network, &request.deployment)?;
106 let (source, registry_json) = query_registry(request, &state.root_canister_id)?;
107 installed_deployment_resolution(state, source, registry_json)
108}
109
110pub fn resolve_installed_deployment_from_root(
111 request: &InstalledDeploymentRequest,
112 icp_root: &Path,
113) -> Result<InstalledDeploymentResolution, InstalledDeploymentError> {
114 let state =
115 read_installed_deployment_state_from_root(&request.network, &request.deployment, icp_root)?;
116 let (source, registry_json) =
117 query_registry_from_root(request, &state.root_canister_id, icp_root)?;
118 installed_deployment_resolution(state, source, registry_json)
119}
120
121fn installed_deployment_resolution(
122 state: InstallState,
123 source: InstalledDeploymentSource,
124 registry_json: String,
125) -> Result<InstalledDeploymentResolution, InstalledDeploymentError> {
126 let entries = parse_registry_entries(®istry_json)?;
127 let registry = InstalledDeploymentRegistry {
128 root_canister_id: state.root_canister_id.clone(),
129 entries,
130 };
131 let topology = ResolvedDeploymentTopology::from_registry(®istry);
132 Ok(InstalledDeploymentResolution {
133 source,
134 state,
135 registry,
136 topology,
137 })
138}
139
140pub fn read_installed_deployment_state(
141 network: &str,
142 deployment: &str,
143) -> Result<InstallState, InstalledDeploymentError> {
144 read_named_deployment_install_state(network, deployment)
145 .map_err(|err| InstalledDeploymentError::InstallState(err.to_string()))?
146 .ok_or_else(|| InstalledDeploymentError::NoInstalledDeployment {
147 network: network.to_string(),
148 deployment: deployment.to_string(),
149 })
150}
151
152pub fn read_installed_deployment_state_from_root(
153 network: &str,
154 deployment: &str,
155 icp_root: &Path,
156) -> Result<InstallState, InstalledDeploymentError> {
157 read_named_deployment_install_state_from_root(icp_root, network, deployment)
158 .map_err(|err| InstalledDeploymentError::InstallState(err.to_string()))?
159 .ok_or_else(|| InstalledDeploymentError::NoInstalledDeployment {
160 network: network.to_string(),
161 deployment: deployment.to_string(),
162 })
163}
164
165impl ResolvedDeploymentTopology {
166 fn from_registry(registry: &InstalledDeploymentRegistry) -> Self {
167 let mut children_by_parent = BTreeMap::<Option<String>, Vec<String>>::new();
168 let mut roles_by_canister = BTreeMap::new();
169 for entry in ®istry.entries {
170 children_by_parent
171 .entry(entry.parent_pid.clone())
172 .or_default()
173 .push(entry.pid.clone());
174 if let Some(role) = &entry.role {
175 roles_by_canister.insert(entry.pid.clone(), role.clone());
176 }
177 }
178 for children in children_by_parent.values_mut() {
179 children.sort();
180 }
181 Self {
182 root_canister_id: registry.root_canister_id.clone(),
183 children_by_parent,
184 roles_by_canister,
185 }
186 }
187}
188
189fn query_registry(
190 request: &InstalledDeploymentRequest,
191 root: &str,
192) -> Result<(InstalledDeploymentSource, String), InstalledDeploymentError> {
193 if replica_query::should_use_local_replica_query(Some(&request.network)) {
194 return replica_query::query_subnet_registry_json(Some(&request.network), root)
195 .map(|registry| (InstalledDeploymentSource::LocalReplica, registry))
196 .map_err(|err| local_registry_error(request, root, err.to_string()));
197 }
198
199 IcpCli::new(&request.icp, None, Some(request.network.clone()))
200 .canister_query_output(root, "canic_subnet_registry", Some("json"))
201 .map(|registry| (InstalledDeploymentSource::IcpCli, registry))
202 .map_err(installed_deployment_icp_error)
203}
204
205fn query_registry_from_root(
206 request: &InstalledDeploymentRequest,
207 root: &str,
208 icp_root: &Path,
209) -> Result<(InstalledDeploymentSource, String), InstalledDeploymentError> {
210 if replica_query::should_use_local_replica_query(Some(&request.network)) {
211 return replica_query::query_subnet_registry_json_from_root(
212 Some(&request.network),
213 root,
214 icp_root,
215 )
216 .map(|registry| (InstalledDeploymentSource::LocalReplica, registry))
217 .map_err(|err| local_registry_error(request, root, err.to_string()));
218 }
219
220 IcpCli::new(&request.icp, None, Some(request.network.clone()))
221 .with_cwd(icp_root)
222 .canister_query_output_with_candid(
223 root,
224 "canic_subnet_registry",
225 Some("json"),
226 existing_local_canister_candid_path(icp_root, &request.network, "root").as_deref(),
227 )
228 .map(|registry| (InstalledDeploymentSource::IcpCli, registry))
229 .map_err(installed_deployment_icp_error)
230}
231
232fn local_registry_error(
233 request: &InstalledDeploymentRequest,
234 root: &str,
235 error: String,
236) -> InstalledDeploymentError {
237 if request.detect_lost_local_root && is_canister_not_found_error(&error) {
238 return InstalledDeploymentError::LostLocalDeployment {
239 deployment: request.deployment.clone(),
240 network: request.network.clone(),
241 root: root.to_string(),
242 };
243 }
244 InstalledDeploymentError::ReplicaQuery(error)
245}
246
247fn is_canister_not_found_error(error: &str) -> bool {
248 error.contains("Canister ") && error.contains(" not found")
249}
250
251fn installed_deployment_icp_error(error: IcpCommandError) -> InstalledDeploymentError {
252 match error {
253 IcpCommandError::Io(err) => InstalledDeploymentError::Io(err),
254 IcpCommandError::Failed { command, stderr } => {
255 InstalledDeploymentError::IcpFailed { command, stderr }
256 }
257 IcpCommandError::Json {
258 command, output, ..
259 } => InstalledDeploymentError::IcpFailed {
260 command,
261 stderr: output,
262 },
263 error @ (IcpCommandError::MissingCli { .. }
264 | IcpCommandError::IncompatibleCliVersion { .. }) => InstalledDeploymentError::IcpFailed {
265 command: "icp --version".to_string(),
266 stderr: error.to_string(),
267 },
268 IcpCommandError::SnapshotIdUnavailable { output } => InstalledDeploymentError::IcpFailed {
269 command: "icp canister snapshot create".to_string(),
270 stderr: output,
271 },
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
281 fn topology_indexes_registry_entries() {
282 let registry = InstalledDeploymentRegistry {
283 root_canister_id: "root-id".to_string(),
284 entries: vec![
285 RegistryEntry {
286 pid: "child-b".to_string(),
287 role: Some("worker".to_string()),
288 kind: None,
289 parent_pid: Some("root-id".to_string()),
290 module_hash: None,
291 },
292 RegistryEntry {
293 pid: "root-id".to_string(),
294 role: Some("root".to_string()),
295 kind: None,
296 parent_pid: None,
297 module_hash: None,
298 },
299 RegistryEntry {
300 pid: "child-a".to_string(),
301 role: Some("app".to_string()),
302 kind: None,
303 parent_pid: Some("root-id".to_string()),
304 module_hash: None,
305 },
306 ],
307 };
308
309 let topology = ResolvedDeploymentTopology::from_registry(®istry);
310
311 assert_eq!(
312 topology
313 .children_by_parent
314 .get(&Some("root-id".to_string())),
315 Some(&vec!["child-a".to_string(), "child-b".to_string()])
316 );
317 assert_eq!(topology.roles_by_canister["child-a"], "app");
318 assert_eq!(topology.root_canister_id, "root-id");
319 }
320
321 #[test]
323 fn detects_local_canister_not_found_error() {
324 assert!(is_canister_not_found_error(
325 "local replica rejected query: code=3 message=Canister uxrrr-q7777-77774-qaaaq-cai not found"
326 ));
327 assert!(!is_canister_not_found_error(
328 "local replica rejected query: code=5 message=some other failure"
329 ));
330 }
331}