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 environment: 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 environment {environment}")]
78    NoInstalledDeployment {
79        environment: String,
80        deployment: String,
81    },
82
83    #[error("failed to read canic deployment state: {0}")]
84    InstallState(#[from] InstallStateError),
85
86    #[error("local replica query failed: {0}")]
87    ReplicaQuery(#[source] ReplicaQueryError),
88
89    #[error(transparent)]
90    Icp(#[from] IcpCommandError),
91
92    #[error(
93        "deployment target {deployment} points to root {root}, but that canister is not present on environment {environment}"
94    )]
95    LostLocalDeployment {
96        deployment: String,
97        environment: String,
98        root: String,
99    },
100
101    #[error(transparent)]
102    Registry(#[from] RegistryParseError),
103
104    #[error(transparent)]
105    Io(#[from] std::io::Error),
106}
107
108pub fn resolve_installed_deployment(
109    request: &InstalledDeploymentRequest,
110) -> Result<InstalledDeploymentResolution, InstalledDeploymentError> {
111    let state = read_installed_deployment_state(&request.environment, &request.deployment)?;
112    let (source, entries) = query_registry(request, &state.root_canister_id)?;
113    Ok(installed_deployment_resolution(state, source, entries))
114}
115
116pub fn resolve_installed_deployment_from_root(
117    request: &InstalledDeploymentRequest,
118    icp_root: &Path,
119) -> Result<InstalledDeploymentResolution, InstalledDeploymentError> {
120    let state = read_installed_deployment_state_from_root(
121        &request.environment,
122        &request.deployment,
123        icp_root,
124    )?;
125    let (source, entries) = query_registry_from_root(request, &state.root_canister_id, icp_root)?;
126    Ok(installed_deployment_resolution(state, source, entries))
127}
128
129fn installed_deployment_resolution(
130    state: InstallState,
131    source: InstalledDeploymentSource,
132    entries: Vec<RegistryEntry>,
133) -> InstalledDeploymentResolution {
134    let registry = InstalledDeploymentRegistry {
135        root_canister_id: state.root_canister_id.clone(),
136        entries,
137    };
138    let topology = ResolvedDeploymentTopology::from_registry(&registry);
139    InstalledDeploymentResolution {
140        source,
141        state,
142        registry,
143        topology,
144    }
145}
146
147pub fn read_installed_deployment_state(
148    environment: &str,
149    deployment: &str,
150) -> Result<InstallState, InstalledDeploymentError> {
151    read_named_deployment_install_state(environment, deployment)
152        .map_err(InstalledDeploymentError::InstallState)?
153        .ok_or_else(|| InstalledDeploymentError::NoInstalledDeployment {
154            environment: environment.to_string(),
155            deployment: deployment.to_string(),
156        })
157}
158
159pub fn read_installed_deployment_state_from_root(
160    environment: &str,
161    deployment: &str,
162    icp_root: &Path,
163) -> Result<InstallState, InstalledDeploymentError> {
164    read_named_deployment_install_state_from_root(icp_root, environment, deployment)
165        .map_err(InstalledDeploymentError::InstallState)?
166        .ok_or_else(|| InstalledDeploymentError::NoInstalledDeployment {
167            environment: environment.to_string(),
168            deployment: deployment.to_string(),
169        })
170}
171
172impl ResolvedDeploymentTopology {
173    fn from_registry(registry: &InstalledDeploymentRegistry) -> Self {
174        let mut children_by_parent = BTreeMap::<Option<String>, Vec<String>>::new();
175        let mut roles_by_canister = BTreeMap::new();
176        for entry in &registry.entries {
177            children_by_parent
178                .entry(entry.parent_pid.clone())
179                .or_default()
180                .push(entry.pid.clone());
181            if let Some(role) = &entry.role {
182                roles_by_canister.insert(entry.pid.clone(), role.clone());
183            }
184        }
185        for children in children_by_parent.values_mut() {
186            children.sort();
187        }
188        Self {
189            root_canister_id: registry.root_canister_id.clone(),
190            children_by_parent,
191            roles_by_canister,
192        }
193    }
194}
195
196fn query_registry(
197    request: &InstalledDeploymentRequest,
198    root: &str,
199) -> Result<(InstalledDeploymentSource, Vec<RegistryEntry>), InstalledDeploymentError> {
200    let icp = IcpCli::new(&request.icp, Some(request.environment.clone()));
201    let query = query_subnet_registry(&icp, root, &request.environment, None, None)
202        .map_err(|err| installed_deployment_registry_error(request, root, err))?;
203    Ok((installed_deployment_source(query.source), query.entries))
204}
205
206fn query_registry_from_root(
207    request: &InstalledDeploymentRequest,
208    root: &str,
209    icp_root: &Path,
210) -> Result<(InstalledDeploymentSource, Vec<RegistryEntry>), InstalledDeploymentError> {
211    let icp = IcpCli::new(&request.icp, Some(request.environment.clone())).with_cwd(icp_root);
212    let candid_path = existing_local_canister_candid_path(icp_root, &request.environment, "root");
213    let query = query_subnet_registry(
214        &icp,
215        root,
216        &request.environment,
217        Some(icp_root),
218        candid_path.as_deref(),
219    )
220    .map_err(|err| installed_deployment_registry_error(request, root, err))?;
221    Ok((installed_deployment_source(query.source), query.entries))
222}
223
224const fn installed_deployment_source(
225    source: SubnetRegistryQuerySource,
226) -> InstalledDeploymentSource {
227    match source {
228        SubnetRegistryQuerySource::LocalReplica => InstalledDeploymentSource::LocalReplica,
229        SubnetRegistryQuerySource::IcpCli => InstalledDeploymentSource::IcpCli,
230    }
231}
232
233fn installed_deployment_registry_error(
234    request: &InstalledDeploymentRequest,
235    root: &str,
236    error: SubnetRegistryQueryError,
237) -> InstalledDeploymentError {
238    match error {
239        SubnetRegistryQueryError::Replica(err) => local_registry_error(request, root, err),
240        SubnetRegistryQueryError::Icp(err) => InstalledDeploymentError::Icp(err),
241        SubnetRegistryQueryError::Registry(err) => InstalledDeploymentError::Registry(err),
242    }
243}
244
245fn local_registry_error(
246    request: &InstalledDeploymentRequest,
247    root: &str,
248    error: ReplicaQueryError,
249) -> InstalledDeploymentError {
250    if request.detect_lost_local_root && is_missing_destination_error(&error) {
251        return InstalledDeploymentError::LostLocalDeployment {
252            deployment: request.deployment.clone(),
253            environment: request.environment.clone(),
254            root: root.to_string(),
255        };
256    }
257    InstalledDeploymentError::ReplicaQuery(error)
258}
259
260const fn is_missing_destination_error(error: &ReplicaQueryError) -> bool {
261    matches!(
262        error,
263        ReplicaQueryError::Rejected {
264            code: IC_REJECT_CODE_DESTINATION_INVALID,
265            ..
266        }
267    )
268}
269
270#[cfg(test)]
271mod tests;