Skip to main content

canic_backup/discovery/
mod.rs

1use crate::{
2    manifest::{
3        DeploymentMember, DeploymentSection, IdentityMode, SourceSnapshot, VerificationCheck,
4    },
5    registry::RegistryEntry,
6    topology::{TopologyHasher, TopologyRecord},
7};
8use std::collections::{BTreeMap, BTreeSet, VecDeque};
9use thiserror::Error as ThisError;
10
11///
12/// DiscoveredFleet
13///
14
15#[derive(Clone, Debug)]
16pub struct DiscoveredFleet {
17    pub topology_records: Vec<TopologyRecord>,
18    pub members: Vec<DiscoveredMember>,
19}
20
21impl DiscoveredFleet {
22    /// Convert discovered topology and member policy into a manifest fleet section.
23    pub fn into_deployment_section(self) -> Result<DeploymentSection, DiscoveryError> {
24        validate_discovered_members(&self.members)?;
25
26        let topology_hash = TopologyHasher::hash(&self.topology_records);
27        let members = self
28            .members
29            .into_iter()
30            .map(DiscoveredMember::into_deployment_member)
31            .collect();
32
33        Ok(DeploymentSection {
34            topology_hash_algorithm: topology_hash.algorithm,
35            topology_hash_input: topology_hash.input,
36            discovery_topology_hash: topology_hash.hash.clone(),
37            pre_snapshot_topology_hash: topology_hash.hash.clone(),
38            topology_hash: topology_hash.hash,
39            members,
40        })
41    }
42}
43
44///
45/// DiscoveredMember
46///
47
48#[derive(Clone, Debug)]
49pub struct DiscoveredMember {
50    pub role: String,
51    pub canister_id: String,
52    pub parent_canister_id: Option<String>,
53    pub subnet_canister_id: Option<String>,
54    pub controller_hint: Option<String>,
55    pub identity_mode: IdentityMode,
56    pub verification_checks: Vec<VerificationCheck>,
57    pub snapshot_plan: SnapshotPlan,
58}
59
60impl DiscoveredMember {
61    /// Project this discovery member into the manifest restore contract.
62    fn into_deployment_member(self) -> DeploymentMember {
63        DeploymentMember {
64            role: self.role,
65            canister_id: self.canister_id,
66            parent_canister_id: self.parent_canister_id,
67            subnet_canister_id: self.subnet_canister_id,
68            controller_hint: self.controller_hint,
69            identity_mode: self.identity_mode,
70            verification_checks: self.verification_checks,
71            source_snapshot: SourceSnapshot {
72                snapshot_id: self.snapshot_plan.snapshot_id,
73                module_hash: self.snapshot_plan.module_hash,
74                code_version: self.snapshot_plan.code_version,
75                artifact_path: self.snapshot_plan.artifact_path,
76                checksum_algorithm: self.snapshot_plan.checksum_algorithm,
77                checksum: self.snapshot_plan.checksum,
78            },
79        }
80    }
81}
82
83///
84/// SnapshotPlan
85///
86
87#[derive(Clone, Debug)]
88pub struct SnapshotPlan {
89    pub snapshot_id: String,
90    pub module_hash: Option<String>,
91    pub code_version: Option<String>,
92    pub artifact_path: String,
93    pub checksum_algorithm: String,
94    pub checksum: Option<String>,
95}
96
97///
98/// SnapshotTarget
99///
100
101#[derive(Clone, Debug, Eq, PartialEq)]
102pub struct SnapshotTarget {
103    pub canister_id: String,
104    pub role: Option<String>,
105    pub parent_canister_id: Option<String>,
106    pub module_hash: Option<String>,
107}
108
109///
110/// DiscoveryError
111///
112
113#[derive(Debug, ThisError)]
114pub enum DiscoveryError {
115    #[error("discovered fleet has no members")]
116    EmptyFleet,
117
118    #[error("duplicate discovered canister id {0}")]
119    DuplicateCanisterId(String),
120
121    #[error("discovered member {0} has no verification checks")]
122    MissingVerificationChecks(String),
123
124    #[error("registry JSON did not contain the requested canister {0}")]
125    CanisterNotInRegistry(String),
126
127    #[error(transparent)]
128    Json(#[from] serde_json::Error),
129}
130
131/// Resolve selected target and children from registry entries.
132pub fn targets_from_registry(
133    registry: &[RegistryEntry],
134    canister_id: &str,
135    recursive: bool,
136) -> Result<Vec<SnapshotTarget>, DiscoveryError> {
137    let by_pid = registry
138        .iter()
139        .map(|entry| (entry.pid.as_str(), entry))
140        .collect::<BTreeMap<_, _>>();
141
142    let root = by_pid
143        .get(canister_id)
144        .ok_or_else(|| DiscoveryError::CanisterNotInRegistry(canister_id.to_string()))?;
145
146    let mut targets = Vec::new();
147    let mut seen = BTreeSet::new();
148    targets.push(SnapshotTarget {
149        canister_id: root.pid.clone(),
150        role: root.role.clone(),
151        parent_canister_id: root.parent_pid.clone(),
152        module_hash: root.module_hash.clone(),
153    });
154    seen.insert(root.pid.clone());
155
156    let mut queue = VecDeque::from([root.pid.clone()]);
157    while let Some(parent) = queue.pop_front() {
158        for child in registry
159            .iter()
160            .filter(|entry| entry.parent_pid.as_deref() == Some(parent.as_str()))
161        {
162            if seen.insert(child.pid.clone()) {
163                targets.push(SnapshotTarget {
164                    canister_id: child.pid.clone(),
165                    role: child.role.clone(),
166                    parent_canister_id: child.parent_pid.clone(),
167                    module_hash: child.module_hash.clone(),
168                });
169                if recursive {
170                    queue.push_back(child.pid.clone());
171                }
172            }
173        }
174    }
175
176    Ok(targets)
177}
178
179// Validate discovery output before building a manifest projection.
180fn validate_discovered_members(members: &[DiscoveredMember]) -> Result<(), DiscoveryError> {
181    if members.is_empty() {
182        return Err(DiscoveryError::EmptyFleet);
183    }
184
185    let mut canister_ids = BTreeSet::new();
186    for member in members {
187        if !canister_ids.insert(member.canister_id.clone()) {
188            return Err(DiscoveryError::DuplicateCanisterId(
189                member.canister_id.clone(),
190            ));
191        }
192        if member.verification_checks.is_empty() {
193            return Err(DiscoveryError::MissingVerificationChecks(
194                member.canister_id.clone(),
195            ));
196        }
197    }
198
199    Ok(())
200}
201
202#[cfg(test)]
203mod tests;