canic_host/installed_deployment/
mod.rs1use crate::{
2 icp::{IcpCli, IcpCommandError, existing_local_canister_candid_path},
3 install_root::{
4 InstallState, InstallStateError, read_named_deployment_install_state,
5 read_named_deployment_install_state_from_root,
6 },
7 registry::{RegistryEntry, RegistryParseError, parse_registry_entries},
8 replica_query::ReplicaQueryError,
9 subnet_registry::{
10 SubnetRegistryQueryError, SubnetRegistryQuerySource, query_subnet_registry_json,
11 },
12};
13use std::{collections::BTreeMap, path::Path};
14use thiserror::Error as ThisError;
15
16const IC_REJECT_CODE_DESTINATION_INVALID: u64 = 3;
17
18#[derive(Clone, Debug, Eq, PartialEq)]
23pub struct InstalledDeploymentRequest {
24 pub deployment: String,
25 pub network: String,
26 pub icp: String,
27 pub detect_lost_local_root: bool,
28}
29
30#[derive(Clone, Debug, Eq, PartialEq)]
35pub struct InstalledDeploymentResolution {
36 pub source: InstalledDeploymentSource,
37 pub state: InstallState,
38 pub registry: InstalledDeploymentRegistry,
39 pub topology: ResolvedDeploymentTopology,
40}
41
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub enum InstalledDeploymentSource {
48 LocalReplica,
49 IcpCli,
50}
51
52#[derive(Clone, Debug, Eq, PartialEq)]
57pub struct InstalledDeploymentRegistry {
58 pub root_canister_id: String,
59 pub entries: Vec<RegistryEntry>,
60}
61
62#[derive(Clone, Debug, Eq, PartialEq)]
67pub struct ResolvedDeploymentTopology {
68 pub root_canister_id: String,
69 pub children_by_parent: BTreeMap<Option<String>, Vec<String>>,
70 pub roles_by_canister: BTreeMap<String, String>,
71}
72
73#[derive(Debug, ThisError)]
78pub enum InstalledDeploymentError {
79 #[error("deployment target {deployment} is not installed on network {network}")]
80 NoInstalledDeployment { network: String, deployment: String },
81
82 #[error("failed to read canic deployment state: {0}")]
83 InstallState(#[from] InstallStateError),
84
85 #[error("local replica query failed: {0}")]
86 ReplicaQuery(#[source] ReplicaQueryError),
87
88 #[error(transparent)]
89 Icp(#[from] IcpCommandError),
90
91 #[error(
92 "deployment target {deployment} points to root {root}, but that canister is not present on network {network}"
93 )]
94 LostLocalDeployment {
95 deployment: String,
96 network: String,
97 root: String,
98 },
99
100 #[error(transparent)]
101 Registry(#[from] RegistryParseError),
102
103 #[error(transparent)]
104 Io(#[from] std::io::Error),
105}
106
107pub fn resolve_installed_deployment(
108 request: &InstalledDeploymentRequest,
109) -> Result<InstalledDeploymentResolution, InstalledDeploymentError> {
110 let state = read_installed_deployment_state(&request.network, &request.deployment)?;
111 let (source, registry_json) = query_registry(request, &state.root_canister_id)?;
112 installed_deployment_resolution(state, source, registry_json)
113}
114
115pub fn resolve_installed_deployment_from_root(
116 request: &InstalledDeploymentRequest,
117 icp_root: &Path,
118) -> Result<InstalledDeploymentResolution, InstalledDeploymentError> {
119 let state =
120 read_installed_deployment_state_from_root(&request.network, &request.deployment, icp_root)?;
121 let (source, registry_json) =
122 query_registry_from_root(request, &state.root_canister_id, icp_root)?;
123 installed_deployment_resolution(state, source, registry_json)
124}
125
126fn installed_deployment_resolution(
127 state: InstallState,
128 source: InstalledDeploymentSource,
129 registry_json: String,
130) -> Result<InstalledDeploymentResolution, InstalledDeploymentError> {
131 let entries = parse_registry_entries(®istry_json)?;
132 let registry = InstalledDeploymentRegistry {
133 root_canister_id: state.root_canister_id.clone(),
134 entries,
135 };
136 let topology = ResolvedDeploymentTopology::from_registry(®istry);
137 Ok(InstalledDeploymentResolution {
138 source,
139 state,
140 registry,
141 topology,
142 })
143}
144
145pub fn read_installed_deployment_state(
146 network: &str,
147 deployment: &str,
148) -> Result<InstallState, InstalledDeploymentError> {
149 read_named_deployment_install_state(network, deployment)
150 .map_err(InstalledDeploymentError::InstallState)?
151 .ok_or_else(|| InstalledDeploymentError::NoInstalledDeployment {
152 network: network.to_string(),
153 deployment: deployment.to_string(),
154 })
155}
156
157pub fn read_installed_deployment_state_from_root(
158 network: &str,
159 deployment: &str,
160 icp_root: &Path,
161) -> Result<InstallState, InstalledDeploymentError> {
162 read_named_deployment_install_state_from_root(icp_root, network, deployment)
163 .map_err(InstalledDeploymentError::InstallState)?
164 .ok_or_else(|| InstalledDeploymentError::NoInstalledDeployment {
165 network: network.to_string(),
166 deployment: deployment.to_string(),
167 })
168}
169
170impl ResolvedDeploymentTopology {
171 fn from_registry(registry: &InstalledDeploymentRegistry) -> Self {
172 let mut children_by_parent = BTreeMap::<Option<String>, Vec<String>>::new();
173 let mut roles_by_canister = BTreeMap::new();
174 for entry in ®istry.entries {
175 children_by_parent
176 .entry(entry.parent_pid.clone())
177 .or_default()
178 .push(entry.pid.clone());
179 if let Some(role) = &entry.role {
180 roles_by_canister.insert(entry.pid.clone(), role.clone());
181 }
182 }
183 for children in children_by_parent.values_mut() {
184 children.sort();
185 }
186 Self {
187 root_canister_id: registry.root_canister_id.clone(),
188 children_by_parent,
189 roles_by_canister,
190 }
191 }
192}
193
194fn query_registry(
195 request: &InstalledDeploymentRequest,
196 root: &str,
197) -> Result<(InstalledDeploymentSource, String), InstalledDeploymentError> {
198 let icp = IcpCli::new(&request.icp, None, Some(request.network.clone()));
199 let query = query_subnet_registry_json(&icp, root, &request.network, None, None)
200 .map_err(|err| installed_deployment_registry_error(request, root, err))?;
201 Ok((
202 installed_deployment_source(query.source),
203 query.registry_json,
204 ))
205}
206
207fn query_registry_from_root(
208 request: &InstalledDeploymentRequest,
209 root: &str,
210 icp_root: &Path,
211) -> Result<(InstalledDeploymentSource, String), InstalledDeploymentError> {
212 let icp = IcpCli::new(&request.icp, None, Some(request.network.clone())).with_cwd(icp_root);
213 let candid_path = existing_local_canister_candid_path(icp_root, &request.network, "root");
214 let query = query_subnet_registry_json(
215 &icp,
216 root,
217 &request.network,
218 Some(icp_root),
219 candid_path.as_deref(),
220 )
221 .map_err(|err| installed_deployment_registry_error(request, root, err))?;
222 Ok((
223 installed_deployment_source(query.source),
224 query.registry_json,
225 ))
226}
227
228const fn installed_deployment_source(
229 source: SubnetRegistryQuerySource,
230) -> InstalledDeploymentSource {
231 match source {
232 SubnetRegistryQuerySource::LocalReplica => InstalledDeploymentSource::LocalReplica,
233 SubnetRegistryQuerySource::IcpCli => InstalledDeploymentSource::IcpCli,
234 }
235}
236
237fn installed_deployment_registry_error(
238 request: &InstalledDeploymentRequest,
239 root: &str,
240 error: SubnetRegistryQueryError,
241) -> InstalledDeploymentError {
242 match error {
243 SubnetRegistryQueryError::Replica(err) => local_registry_error(request, root, err),
244 SubnetRegistryQueryError::Icp(err) => InstalledDeploymentError::Icp(err),
245 }
246}
247
248fn local_registry_error(
249 request: &InstalledDeploymentRequest,
250 root: &str,
251 error: ReplicaQueryError,
252) -> InstalledDeploymentError {
253 if request.detect_lost_local_root && is_missing_destination_error(&error) {
254 return InstalledDeploymentError::LostLocalDeployment {
255 deployment: request.deployment.clone(),
256 network: request.network.clone(),
257 root: root.to_string(),
258 };
259 }
260 InstalledDeploymentError::ReplicaQuery(error)
261}
262
263const fn is_missing_destination_error(error: &ReplicaQueryError) -> bool {
264 matches!(
265 error,
266 ReplicaQueryError::Rejected {
267 code: IC_REJECT_CODE_DESTINATION_INVALID,
268 ..
269 }
270 )
271}
272
273#[cfg(test)]
274mod tests;