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, 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///
19/// InstalledDeploymentRequest
20///
21
22#[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///
31/// InstalledDeploymentResolution
32///
33
34#[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///
43/// InstalledDeploymentSource
44///
45
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub enum InstalledDeploymentSource {
48    LocalReplica,
49    IcpCli,
50}
51
52///
53/// InstalledDeploymentRegistry
54///
55
56#[derive(Clone, Debug, Eq, PartialEq)]
57pub struct InstalledDeploymentRegistry {
58    pub root_canister_id: String,
59    pub entries: Vec<RegistryEntry>,
60}
61
62///
63/// ResolvedDeploymentTopology
64///
65
66#[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///
74/// InstalledDeploymentError
75///
76
77#[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(&registry_json)?;
132    let registry = InstalledDeploymentRegistry {
133        root_canister_id: state.root_canister_id.clone(),
134        entries,
135    };
136    let topology = ResolvedDeploymentTopology::from_registry(&registry);
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 &registry.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;