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