1use crate::{
2 icp::{IcpCli, IcpCommandError},
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(request, 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(request, state, source, registry_json)
119}
120
121fn installed_deployment_resolution(
122 _request: &InstalledDeploymentRequest,
123 state: InstallState,
124 source: InstalledDeploymentSource,
125 registry_json: String,
126) -> Result<InstalledDeploymentResolution, InstalledDeploymentError> {
127 let entries = parse_registry_entries(®istry_json)?;
128 let registry = InstalledDeploymentRegistry {
129 root_canister_id: state.root_canister_id.clone(),
130 entries,
131 };
132 let topology = ResolvedDeploymentTopology::from_registry(®istry);
133 Ok(InstalledDeploymentResolution {
134 source,
135 state,
136 registry,
137 topology,
138 })
139}
140
141pub fn read_installed_deployment_state(
142 network: &str,
143 deployment: &str,
144) -> Result<InstallState, InstalledDeploymentError> {
145 read_named_deployment_install_state(network, deployment)
146 .map_err(|err| InstalledDeploymentError::InstallState(err.to_string()))?
147 .ok_or_else(|| InstalledDeploymentError::NoInstalledDeployment {
148 network: network.to_string(),
149 deployment: deployment.to_string(),
150 })
151}
152
153pub fn read_installed_deployment_state_from_root(
154 network: &str,
155 deployment: &str,
156 icp_root: &Path,
157) -> Result<InstallState, InstalledDeploymentError> {
158 read_named_deployment_install_state_from_root(icp_root, network, deployment)
159 .map_err(|err| InstalledDeploymentError::InstallState(err.to_string()))?
160 .ok_or_else(|| InstalledDeploymentError::NoInstalledDeployment {
161 network: network.to_string(),
162 deployment: deployment.to_string(),
163 })
164}
165
166impl ResolvedDeploymentTopology {
167 fn from_registry(registry: &InstalledDeploymentRegistry) -> Self {
168 let mut children_by_parent = BTreeMap::<Option<String>, Vec<String>>::new();
169 let mut roles_by_canister = BTreeMap::new();
170 for entry in ®istry.entries {
171 children_by_parent
172 .entry(entry.parent_pid.clone())
173 .or_default()
174 .push(entry.pid.clone());
175 if let Some(role) = &entry.role {
176 roles_by_canister.insert(entry.pid.clone(), role.clone());
177 }
178 }
179 for children in children_by_parent.values_mut() {
180 children.sort();
181 }
182 Self {
183 root_canister_id: registry.root_canister_id.clone(),
184 children_by_parent,
185 roles_by_canister,
186 }
187 }
188}
189
190fn query_registry(
191 request: &InstalledDeploymentRequest,
192 root: &str,
193) -> Result<(InstalledDeploymentSource, String), InstalledDeploymentError> {
194 if replica_query::should_use_local_replica_query(Some(&request.network)) {
195 return replica_query::query_subnet_registry_json(Some(&request.network), root)
196 .map(|registry| (InstalledDeploymentSource::LocalReplica, registry))
197 .map_err(|err| local_registry_error(request, root, err.to_string()));
198 }
199
200 IcpCli::new(&request.icp, None, Some(request.network.clone()))
201 .canister_query_output(root, "canic_subnet_registry", Some("json"))
202 .map(|registry| (InstalledDeploymentSource::IcpCli, registry))
203 .map_err(installed_deployment_icp_error)
204}
205
206fn query_registry_from_root(
207 request: &InstalledDeploymentRequest,
208 root: &str,
209 icp_root: &Path,
210) -> Result<(InstalledDeploymentSource, String), InstalledDeploymentError> {
211 if replica_query::should_use_local_replica_query(Some(&request.network)) {
212 return replica_query::query_subnet_registry_json_from_root(
213 Some(&request.network),
214 root,
215 icp_root,
216 )
217 .map(|registry| (InstalledDeploymentSource::LocalReplica, registry))
218 .map_err(|err| local_registry_error(request, root, err.to_string()));
219 }
220
221 IcpCli::new(&request.icp, None, Some(request.network.clone()))
222 .with_cwd(icp_root)
223 .canister_query_output(root, "canic_subnet_registry", Some("json"))
224 .map(|registry| (InstalledDeploymentSource::IcpCli, registry))
225 .map_err(installed_deployment_icp_error)
226}
227
228fn local_registry_error(
229 request: &InstalledDeploymentRequest,
230 root: &str,
231 error: String,
232) -> InstalledDeploymentError {
233 if request.detect_lost_local_root && is_canister_not_found_error(&error) {
234 return InstalledDeploymentError::LostLocalDeployment {
235 deployment: request.deployment.clone(),
236 network: request.network.clone(),
237 root: root.to_string(),
238 };
239 }
240 InstalledDeploymentError::ReplicaQuery(error)
241}
242
243fn is_canister_not_found_error(error: &str) -> bool {
244 error.contains("Canister ") && error.contains(" not found")
245}
246
247fn installed_deployment_icp_error(error: IcpCommandError) -> InstalledDeploymentError {
248 match error {
249 IcpCommandError::Io(err) => InstalledDeploymentError::Io(err),
250 IcpCommandError::Failed { command, stderr } => {
251 InstalledDeploymentError::IcpFailed { command, stderr }
252 }
253 IcpCommandError::Json {
254 command, output, ..
255 } => InstalledDeploymentError::IcpFailed {
256 command,
257 stderr: output,
258 },
259 IcpCommandError::SnapshotIdUnavailable { output } => InstalledDeploymentError::IcpFailed {
260 command: "icp canister snapshot create".to_string(),
261 stderr: output,
262 },
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
272 fn topology_indexes_registry_entries() {
273 let registry = InstalledDeploymentRegistry {
274 root_canister_id: "root-id".to_string(),
275 entries: vec![
276 RegistryEntry {
277 pid: "child-b".to_string(),
278 role: Some("worker".to_string()),
279 kind: None,
280 parent_pid: Some("root-id".to_string()),
281 module_hash: None,
282 },
283 RegistryEntry {
284 pid: "root-id".to_string(),
285 role: Some("root".to_string()),
286 kind: None,
287 parent_pid: None,
288 module_hash: None,
289 },
290 RegistryEntry {
291 pid: "child-a".to_string(),
292 role: Some("app".to_string()),
293 kind: None,
294 parent_pid: Some("root-id".to_string()),
295 module_hash: None,
296 },
297 ],
298 };
299
300 let topology = ResolvedDeploymentTopology::from_registry(®istry);
301
302 assert_eq!(
303 topology
304 .children_by_parent
305 .get(&Some("root-id".to_string())),
306 Some(&vec!["child-a".to_string(), "child-b".to_string()])
307 );
308 assert_eq!(topology.roles_by_canister["child-a"], "app");
309 assert_eq!(topology.root_canister_id, "root-id");
310 }
311
312 #[test]
314 fn detects_local_canister_not_found_error() {
315 assert!(is_canister_not_found_error(
316 "local replica rejected query: code=3 message=Canister uxrrr-q7777-77774-qaaaq-cai not found"
317 ));
318 assert!(!is_canister_not_found_error(
319 "local replica rejected query: code=5 message=some other failure"
320 ));
321 }
322}