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_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///
16/// InstalledDeploymentRequest
17///
18
19#[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///
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 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(&registry);
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 &registry.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;