Skip to main content

canic_host/installed_deployment/
mod.rs

1use 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///
17/// InstalledDeploymentRequest
18///
19
20#[derive(Clone, Debug, Eq, PartialEq)]
21pub struct InstalledDeploymentRequest {
22    pub deployment: String,
23    pub network: String,
24    pub icp: String,
25    pub detect_lost_local_root: bool,
26}
27
28///
29/// InstalledDeploymentResolution
30///
31
32#[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///
41/// InstalledDeploymentSource
42///
43
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub enum InstalledDeploymentSource {
46    LocalReplica,
47    IcpCli,
48}
49
50///
51/// InstalledDeploymentRegistry
52///
53
54#[derive(Clone, Debug, Eq, PartialEq)]
55pub struct InstalledDeploymentRegistry {
56    pub root_canister_id: String,
57    pub entries: Vec<RegistryEntry>,
58}
59
60///
61/// ResolvedDeploymentTopology
62///
63
64#[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///
72/// InstalledDeploymentError
73///
74
75#[derive(Debug, ThisError)]
76pub enum InstalledDeploymentError {
77    #[error("deployment target {deployment} is not installed on network {network}")]
78    NoInstalledDeployment { network: String, deployment: String },
79
80    #[error("failed to read canic deployment state: {0}")]
81    InstallState(#[from] InstallStateError),
82
83    #[error("local replica query failed: {0}")]
84    ReplicaQuery(#[source] ReplicaQueryError),
85
86    #[error(transparent)]
87    Icp(#[from] IcpCommandError),
88
89    #[error(
90        "deployment target {deployment} points to root {root}, but that canister is not present on network {network}"
91    )]
92    LostLocalDeployment {
93        deployment: String,
94        network: String,
95        root: String,
96    },
97
98    #[error(transparent)]
99    Registry(#[from] RegistryParseError),
100
101    #[error(transparent)]
102    Io(#[from] std::io::Error),
103}
104
105pub fn resolve_installed_deployment(
106    request: &InstalledDeploymentRequest,
107) -> Result<InstalledDeploymentResolution, InstalledDeploymentError> {
108    let state = read_installed_deployment_state(&request.network, &request.deployment)?;
109    let (source, entries) = query_registry(request, &state.root_canister_id)?;
110    Ok(installed_deployment_resolution(state, source, entries))
111}
112
113pub fn resolve_installed_deployment_from_root(
114    request: &InstalledDeploymentRequest,
115    icp_root: &Path,
116) -> Result<InstalledDeploymentResolution, InstalledDeploymentError> {
117    let state =
118        read_installed_deployment_state_from_root(&request.network, &request.deployment, icp_root)?;
119    let (source, entries) = query_registry_from_root(request, &state.root_canister_id, icp_root)?;
120    Ok(installed_deployment_resolution(state, source, entries))
121}
122
123fn installed_deployment_resolution(
124    state: InstallState,
125    source: InstalledDeploymentSource,
126    entries: Vec<RegistryEntry>,
127) -> InstalledDeploymentResolution {
128    let registry = InstalledDeploymentRegistry {
129        root_canister_id: state.root_canister_id.clone(),
130        entries,
131    };
132    let topology = ResolvedDeploymentTopology::from_registry(&registry);
133    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(InstalledDeploymentError::InstallState)?
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(InstalledDeploymentError::InstallState)?
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 &registry.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, Vec<RegistryEntry>), InstalledDeploymentError> {
194    let icp = IcpCli::new(&request.icp, None, Some(request.network.clone()));
195    let query = query_subnet_registry(&icp, root, &request.network, None, None)
196        .map_err(|err| installed_deployment_registry_error(request, root, err))?;
197    Ok((installed_deployment_source(query.source), query.entries))
198}
199
200fn query_registry_from_root(
201    request: &InstalledDeploymentRequest,
202    root: &str,
203    icp_root: &Path,
204) -> Result<(InstalledDeploymentSource, Vec<RegistryEntry>), InstalledDeploymentError> {
205    let icp = IcpCli::new(&request.icp, None, Some(request.network.clone())).with_cwd(icp_root);
206    let candid_path = existing_local_canister_candid_path(icp_root, &request.network, "root");
207    let query = query_subnet_registry(
208        &icp,
209        root,
210        &request.network,
211        Some(icp_root),
212        candid_path.as_deref(),
213    )
214    .map_err(|err| installed_deployment_registry_error(request, root, err))?;
215    Ok((installed_deployment_source(query.source), query.entries))
216}
217
218const fn installed_deployment_source(
219    source: SubnetRegistryQuerySource,
220) -> InstalledDeploymentSource {
221    match source {
222        SubnetRegistryQuerySource::LocalReplica => InstalledDeploymentSource::LocalReplica,
223        SubnetRegistryQuerySource::IcpCli => InstalledDeploymentSource::IcpCli,
224    }
225}
226
227fn installed_deployment_registry_error(
228    request: &InstalledDeploymentRequest,
229    root: &str,
230    error: SubnetRegistryQueryError,
231) -> InstalledDeploymentError {
232    match error {
233        SubnetRegistryQueryError::Replica(err) => local_registry_error(request, root, err),
234        SubnetRegistryQueryError::Icp(err) => InstalledDeploymentError::Icp(err),
235        SubnetRegistryQueryError::Registry(err) => InstalledDeploymentError::Registry(err),
236    }
237}
238
239fn local_registry_error(
240    request: &InstalledDeploymentRequest,
241    root: &str,
242    error: ReplicaQueryError,
243) -> InstalledDeploymentError {
244    if request.detect_lost_local_root && is_missing_destination_error(&error) {
245        return InstalledDeploymentError::LostLocalDeployment {
246            deployment: request.deployment.clone(),
247            network: request.network.clone(),
248            root: root.to_string(),
249        };
250    }
251    InstalledDeploymentError::ReplicaQuery(error)
252}
253
254const fn is_missing_destination_error(error: &ReplicaQueryError) -> bool {
255    matches!(
256        error,
257        ReplicaQueryError::Rejected {
258            code: IC_REJECT_CODE_DESTINATION_INVALID,
259            ..
260        }
261    )
262}
263
264#[cfg(test)]
265mod tests;