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 subnet_registry::{
9 SubnetRegistryQueryError, SubnetRegistryQuerySource, query_subnet_registry_json,
10 },
11};
12use std::{collections::BTreeMap, path::Path};
13use thiserror::Error as ThisError;
14
15#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct InstalledDeploymentRequest {
21 pub deployment: String,
22 pub network: String,
23 pub icp: String,
24 pub detect_lost_local_root: bool,
25}
26
27#[derive(Clone, Debug, Eq, PartialEq)]
32pub struct InstalledDeploymentResolution {
33 pub source: InstalledDeploymentSource,
34 pub state: InstallState,
35 pub registry: InstalledDeploymentRegistry,
36 pub topology: ResolvedDeploymentTopology,
37}
38
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub enum InstalledDeploymentSource {
45 LocalReplica,
46 IcpCli,
47}
48
49#[derive(Clone, Debug, Eq, PartialEq)]
54pub struct InstalledDeploymentRegistry {
55 pub root_canister_id: String,
56 pub entries: Vec<RegistryEntry>,
57}
58
59#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct ResolvedDeploymentTopology {
65 pub root_canister_id: String,
66 pub children_by_parent: BTreeMap<Option<String>, Vec<String>>,
67 pub roles_by_canister: BTreeMap<String, String>,
68}
69
70#[derive(Debug, ThisError)]
75pub enum InstalledDeploymentError {
76 #[error("deployment target {deployment} is not installed on network {network}")]
77 NoInstalledDeployment { network: String, deployment: String },
78
79 #[error("failed to read canic deployment state: {0}")]
80 InstallState(String),
81
82 #[error("local replica query failed: {0}")]
83 ReplicaQuery(String),
84
85 #[error("icp command failed: {command}\n{stderr}")]
86 IcpFailed { command: String, stderr: String },
87
88 #[error(
89 "deployment target {deployment} points to root {root}, but that canister is not present on network {network}"
90 )]
91 LostLocalDeployment {
92 deployment: String,
93 network: String,
94 root: String,
95 },
96
97 #[error(transparent)]
98 Registry(#[from] RegistryParseError),
99
100 #[error(transparent)]
101 Io(#[from] std::io::Error),
102}
103
104pub fn resolve_installed_deployment(
105 request: &InstalledDeploymentRequest,
106) -> Result<InstalledDeploymentResolution, InstalledDeploymentError> {
107 let state = read_installed_deployment_state(&request.network, &request.deployment)?;
108 let (source, registry_json) = query_registry(request, &state.root_canister_id)?;
109 installed_deployment_resolution(state, source, registry_json)
110}
111
112pub fn resolve_installed_deployment_from_root(
113 request: &InstalledDeploymentRequest,
114 icp_root: &Path,
115) -> Result<InstalledDeploymentResolution, InstalledDeploymentError> {
116 let state =
117 read_installed_deployment_state_from_root(&request.network, &request.deployment, icp_root)?;
118 let (source, registry_json) =
119 query_registry_from_root(request, &state.root_canister_id, icp_root)?;
120 installed_deployment_resolution(state, source, registry_json)
121}
122
123fn installed_deployment_resolution(
124 state: InstallState,
125 source: InstalledDeploymentSource,
126 registry_json: String,
127) -> Result<InstalledDeploymentResolution, InstalledDeploymentError> {
128 let entries = parse_registry_entries(®istry_json)?;
129 let registry = InstalledDeploymentRegistry {
130 root_canister_id: state.root_canister_id.clone(),
131 entries,
132 };
133 let topology = ResolvedDeploymentTopology::from_registry(®istry);
134 Ok(InstalledDeploymentResolution {
135 source,
136 state,
137 registry,
138 topology,
139 })
140}
141
142pub fn read_installed_deployment_state(
143 network: &str,
144 deployment: &str,
145) -> Result<InstallState, InstalledDeploymentError> {
146 read_named_deployment_install_state(network, deployment)
147 .map_err(|err| InstalledDeploymentError::InstallState(err.to_string()))?
148 .ok_or_else(|| InstalledDeploymentError::NoInstalledDeployment {
149 network: network.to_string(),
150 deployment: deployment.to_string(),
151 })
152}
153
154pub fn read_installed_deployment_state_from_root(
155 network: &str,
156 deployment: &str,
157 icp_root: &Path,
158) -> Result<InstallState, InstalledDeploymentError> {
159 read_named_deployment_install_state_from_root(icp_root, network, deployment)
160 .map_err(|err| InstalledDeploymentError::InstallState(err.to_string()))?
161 .ok_or_else(|| InstalledDeploymentError::NoInstalledDeployment {
162 network: network.to_string(),
163 deployment: deployment.to_string(),
164 })
165}
166
167impl ResolvedDeploymentTopology {
168 fn from_registry(registry: &InstalledDeploymentRegistry) -> Self {
169 let mut children_by_parent = BTreeMap::<Option<String>, Vec<String>>::new();
170 let mut roles_by_canister = BTreeMap::new();
171 for entry in ®istry.entries {
172 children_by_parent
173 .entry(entry.parent_pid.clone())
174 .or_default()
175 .push(entry.pid.clone());
176 if let Some(role) = &entry.role {
177 roles_by_canister.insert(entry.pid.clone(), role.clone());
178 }
179 }
180 for children in children_by_parent.values_mut() {
181 children.sort();
182 }
183 Self {
184 root_canister_id: registry.root_canister_id.clone(),
185 children_by_parent,
186 roles_by_canister,
187 }
188 }
189}
190
191fn query_registry(
192 request: &InstalledDeploymentRequest,
193 root: &str,
194) -> Result<(InstalledDeploymentSource, String), InstalledDeploymentError> {
195 let icp = IcpCli::new(&request.icp, None, Some(request.network.clone()));
196 let query = query_subnet_registry_json(&icp, root, &request.network, None, None)
197 .map_err(|err| installed_deployment_registry_error(request, root, err))?;
198 Ok((
199 installed_deployment_source(query.source),
200 query.registry_json,
201 ))
202}
203
204fn query_registry_from_root(
205 request: &InstalledDeploymentRequest,
206 root: &str,
207 icp_root: &Path,
208) -> Result<(InstalledDeploymentSource, String), InstalledDeploymentError> {
209 let icp = IcpCli::new(&request.icp, None, Some(request.network.clone())).with_cwd(icp_root);
210 let candid_path = existing_local_canister_candid_path(icp_root, &request.network, "root");
211 let query = query_subnet_registry_json(
212 &icp,
213 root,
214 &request.network,
215 Some(icp_root),
216 candid_path.as_deref(),
217 )
218 .map_err(|err| installed_deployment_registry_error(request, root, err))?;
219 Ok((
220 installed_deployment_source(query.source),
221 query.registry_json,
222 ))
223}
224
225const fn installed_deployment_source(
226 source: SubnetRegistryQuerySource,
227) -> InstalledDeploymentSource {
228 match source {
229 SubnetRegistryQuerySource::LocalReplica => InstalledDeploymentSource::LocalReplica,
230 SubnetRegistryQuerySource::IcpCli => InstalledDeploymentSource::IcpCli,
231 }
232}
233
234fn installed_deployment_registry_error(
235 request: &InstalledDeploymentRequest,
236 root: &str,
237 error: SubnetRegistryQueryError,
238) -> InstalledDeploymentError {
239 match error {
240 SubnetRegistryQueryError::Replica(err) => {
241 local_registry_error(request, root, err.to_string())
242 }
243 SubnetRegistryQueryError::Icp(err) => installed_deployment_icp_error(err),
244 }
245}
246
247fn local_registry_error(
248 request: &InstalledDeploymentRequest,
249 root: &str,
250 error: String,
251) -> InstalledDeploymentError {
252 if request.detect_lost_local_root && is_canister_not_found_error(&error) {
253 return InstalledDeploymentError::LostLocalDeployment {
254 deployment: request.deployment.clone(),
255 network: request.network.clone(),
256 root: root.to_string(),
257 };
258 }
259 InstalledDeploymentError::ReplicaQuery(error)
260}
261
262fn is_canister_not_found_error(error: &str) -> bool {
263 error.contains("Canister ") && error.contains(" not found")
264}
265
266fn installed_deployment_icp_error(error: IcpCommandError) -> InstalledDeploymentError {
267 match error {
268 IcpCommandError::Io(err) => InstalledDeploymentError::Io(err),
269 IcpCommandError::Failed { command, stderr } => {
270 InstalledDeploymentError::IcpFailed { command, stderr }
271 }
272 IcpCommandError::Json {
273 command, output, ..
274 } => InstalledDeploymentError::IcpFailed {
275 command,
276 stderr: output,
277 },
278 error @ (IcpCommandError::MissingCli { .. }
279 | IcpCommandError::IncompatibleCliVersion { .. }) => InstalledDeploymentError::IcpFailed {
280 command: "icp --version".to_string(),
281 stderr: error.to_string(),
282 },
283 IcpCommandError::SnapshotIdUnavailable { output } => InstalledDeploymentError::IcpFailed {
284 command: "icp canister snapshot create".to_string(),
285 stderr: output,
286 },
287 }
288}
289
290#[cfg(test)]
291mod tests;