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