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