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_from_root,
5 },
6 registry::{RegistryEntry, RegistryParseError},
7 replica_query::ReplicaQueryError,
8 subnet_registry::{SubnetRegistryQueryError, SubnetRegistryQuerySource, query_subnet_registry},
9};
10use std::{collections::BTreeMap, path::Path};
11use thiserror::Error as ThisError;
12
13const IC_REJECT_CODE_DESTINATION_INVALID: u64 = 3;
14
15#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct InstalledDeploymentRequest {
21 pub deployment: String,
22 pub environment: 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 environment {environment}")]
77 NoInstalledDeployment {
78 environment: String,
79 deployment: String,
80 },
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 environment {environment}"
93 )]
94 LostLocalDeployment {
95 deployment: String,
96 environment: 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_from_root(
108 request: &InstalledDeploymentRequest,
109 icp_root: &Path,
110) -> Result<InstalledDeploymentResolution, InstalledDeploymentError> {
111 let state = read_installed_deployment_state_from_root(
112 &request.environment,
113 &request.deployment,
114 icp_root,
115 )?;
116 let (source, entries) = query_registry_from_root(request, &state.root_canister_id, icp_root)?;
117 Ok(installed_deployment_resolution(state, source, entries))
118}
119
120fn installed_deployment_resolution(
121 state: InstallState,
122 source: InstalledDeploymentSource,
123 entries: Vec<RegistryEntry>,
124) -> InstalledDeploymentResolution {
125 let registry = InstalledDeploymentRegistry {
126 root_canister_id: state.root_canister_id.clone(),
127 entries,
128 };
129 let topology = ResolvedDeploymentTopology::from_registry(®istry);
130 InstalledDeploymentResolution {
131 source,
132 state,
133 registry,
134 topology,
135 }
136}
137
138pub fn read_installed_deployment_state_from_root(
139 environment: &str,
140 deployment: &str,
141 icp_root: &Path,
142) -> Result<InstallState, InstalledDeploymentError> {
143 read_named_deployment_install_state_from_root(icp_root, environment, deployment)
144 .map_err(InstalledDeploymentError::InstallState)?
145 .ok_or_else(|| InstalledDeploymentError::NoInstalledDeployment {
146 environment: environment.to_string(),
147 deployment: deployment.to_string(),
148 })
149}
150
151impl ResolvedDeploymentTopology {
152 fn from_registry(registry: &InstalledDeploymentRegistry) -> Self {
153 let mut children_by_parent = BTreeMap::<Option<String>, Vec<String>>::new();
154 let mut roles_by_canister = BTreeMap::new();
155 for entry in ®istry.entries {
156 children_by_parent
157 .entry(entry.parent_pid.clone())
158 .or_default()
159 .push(entry.pid.clone());
160 if let Some(role) = &entry.role {
161 roles_by_canister.insert(entry.pid.clone(), role.clone());
162 }
163 }
164 for children in children_by_parent.values_mut() {
165 children.sort();
166 }
167 Self {
168 root_canister_id: registry.root_canister_id.clone(),
169 children_by_parent,
170 roles_by_canister,
171 }
172 }
173}
174
175fn query_registry_from_root(
176 request: &InstalledDeploymentRequest,
177 root: &str,
178 icp_root: &Path,
179) -> Result<(InstalledDeploymentSource, Vec<RegistryEntry>), InstalledDeploymentError> {
180 let icp = IcpCli::new(&request.icp, Some(request.environment.clone())).with_cwd(icp_root);
181 let candid_path = existing_local_canister_candid_path(icp_root, &request.environment, "root");
182 let query = query_subnet_registry(
183 &icp,
184 root,
185 &request.environment,
186 Some(icp_root),
187 candid_path.as_deref(),
188 )
189 .map_err(|err| installed_deployment_registry_error(request, root, err))?;
190 Ok((installed_deployment_source(query.source), query.entries))
191}
192
193const fn installed_deployment_source(
194 source: SubnetRegistryQuerySource,
195) -> InstalledDeploymentSource {
196 match source {
197 SubnetRegistryQuerySource::LocalReplica => InstalledDeploymentSource::LocalReplica,
198 SubnetRegistryQuerySource::IcpCli => InstalledDeploymentSource::IcpCli,
199 }
200}
201
202fn installed_deployment_registry_error(
203 request: &InstalledDeploymentRequest,
204 root: &str,
205 error: SubnetRegistryQueryError,
206) -> InstalledDeploymentError {
207 match error {
208 SubnetRegistryQueryError::Replica(err) => local_registry_error(request, root, err),
209 SubnetRegistryQueryError::Icp(err) => InstalledDeploymentError::Icp(err),
210 SubnetRegistryQueryError::Registry(err) => InstalledDeploymentError::Registry(err),
211 }
212}
213
214fn local_registry_error(
215 request: &InstalledDeploymentRequest,
216 root: &str,
217 error: ReplicaQueryError,
218) -> InstalledDeploymentError {
219 if request.detect_lost_local_root && is_missing_destination_error(&error) {
220 return InstalledDeploymentError::LostLocalDeployment {
221 deployment: request.deployment.clone(),
222 environment: request.environment.clone(),
223 root: root.to_string(),
224 };
225 }
226 InstalledDeploymentError::ReplicaQuery(error)
227}
228
229const fn is_missing_destination_error(error: &ReplicaQueryError) -> bool {
230 matches!(
231 error,
232 ReplicaQueryError::Rejected {
233 code: IC_REJECT_CODE_DESTINATION_INVALID,
234 ..
235 }
236 )
237}
238
239#[cfg(test)]
240mod tests;