canic_backup/discovery/
mod.rs1#[cfg(test)]
8mod tests;
9
10use crate::{
11 manifest::{
12 DeploymentMember, DeploymentSection, IdentityMode, SourceSnapshot, VerificationCheck,
13 },
14 registry::RegistryEntry,
15 topology::{TopologyHasher, TopologyRecord},
16};
17use std::collections::{BTreeMap, BTreeSet, VecDeque};
18use thiserror::Error as ThisError;
19
20#[derive(Clone, Debug)]
28pub struct DiscoveredFleet {
29 pub topology_records: Vec<TopologyRecord>,
30 pub members: Vec<DiscoveredMember>,
31}
32
33impl DiscoveredFleet {
34 pub fn into_deployment_section(self) -> Result<DeploymentSection, DiscoveryError> {
36 validate_discovered_members(&self.members)?;
37
38 let topology_hash = TopologyHasher::hash(&self.topology_records);
39 let members = self
40 .members
41 .into_iter()
42 .map(DiscoveredMember::into_deployment_member)
43 .collect();
44
45 Ok(DeploymentSection {
46 topology_hash_algorithm: topology_hash.algorithm,
47 topology_hash_input: topology_hash.input,
48 discovery_topology_hash: topology_hash.hash.clone(),
49 pre_snapshot_topology_hash: topology_hash.hash.clone(),
50 topology_hash: topology_hash.hash,
51 members,
52 })
53 }
54}
55
56#[derive(Clone, Debug)]
64pub struct DiscoveredMember {
65 pub role: String,
66 pub canister_id: String,
67 pub parent_canister_id: Option<String>,
68 pub subnet_canister_id: Option<String>,
69 pub controller_hint: Option<String>,
70 pub identity_mode: IdentityMode,
71 pub verification_checks: Vec<VerificationCheck>,
72 pub snapshot_plan: SnapshotPlan,
73}
74
75impl DiscoveredMember {
76 fn into_deployment_member(self) -> DeploymentMember {
78 DeploymentMember {
79 role: self.role,
80 canister_id: self.canister_id,
81 parent_canister_id: self.parent_canister_id,
82 subnet_canister_id: self.subnet_canister_id,
83 controller_hint: self.controller_hint,
84 identity_mode: self.identity_mode,
85 verification_checks: self.verification_checks,
86 source_snapshot: SourceSnapshot {
87 snapshot_id: self.snapshot_plan.snapshot_id,
88 module_hash: self.snapshot_plan.module_hash,
89 code_version: self.snapshot_plan.code_version,
90 artifact_path: self.snapshot_plan.artifact_path,
91 checksum_algorithm: self.snapshot_plan.checksum_algorithm,
92 checksum: self.snapshot_plan.checksum,
93 },
94 }
95 }
96}
97
98#[derive(Clone, Debug)]
106pub struct SnapshotPlan {
107 pub snapshot_id: String,
108 pub module_hash: Option<String>,
109 pub code_version: Option<String>,
110 pub artifact_path: String,
111 pub checksum_algorithm: String,
112 pub checksum: Option<String>,
113}
114
115#[derive(Clone, Debug, Eq, PartialEq)]
123pub struct SnapshotTarget {
124 pub canister_id: String,
125 pub role: Option<String>,
126 pub parent_canister_id: Option<String>,
127 pub module_hash: Option<String>,
128}
129
130#[derive(Debug, ThisError)]
138pub enum DiscoveryError {
139 #[error("discovered fleet has no members")]
140 EmptyFleet,
141
142 #[error("duplicate discovered canister id {0}")]
143 DuplicateCanisterId(String),
144
145 #[error("discovered member {0} has no verification checks")]
146 MissingVerificationChecks(String),
147
148 #[error("registry JSON did not contain the requested canister {0}")]
149 CanisterNotInRegistry(String),
150
151 #[error(transparent)]
152 Json(#[from] serde_json::Error),
153}
154
155pub fn targets_from_registry(
157 registry: &[RegistryEntry],
158 canister_id: &str,
159 recursive: bool,
160) -> Result<Vec<SnapshotTarget>, DiscoveryError> {
161 let by_pid = registry
162 .iter()
163 .map(|entry| (entry.pid.as_str(), entry))
164 .collect::<BTreeMap<_, _>>();
165
166 let root = by_pid
167 .get(canister_id)
168 .ok_or_else(|| DiscoveryError::CanisterNotInRegistry(canister_id.to_string()))?;
169
170 let mut targets = Vec::new();
171 let mut seen = BTreeSet::new();
172 targets.push(SnapshotTarget {
173 canister_id: root.pid.clone(),
174 role: root.role.clone(),
175 parent_canister_id: root.parent_pid.clone(),
176 module_hash: root.module_hash.clone(),
177 });
178 seen.insert(root.pid.clone());
179
180 let mut queue = VecDeque::from([root.pid.clone()]);
181 while let Some(parent) = queue.pop_front() {
182 for child in registry
183 .iter()
184 .filter(|entry| entry.parent_pid.as_deref() == Some(parent.as_str()))
185 {
186 if seen.insert(child.pid.clone()) {
187 targets.push(SnapshotTarget {
188 canister_id: child.pid.clone(),
189 role: child.role.clone(),
190 parent_canister_id: child.parent_pid.clone(),
191 module_hash: child.module_hash.clone(),
192 });
193 if recursive {
194 queue.push_back(child.pid.clone());
195 }
196 }
197 }
198 }
199
200 Ok(targets)
201}
202
203fn validate_discovered_members(members: &[DiscoveredMember]) -> Result<(), DiscoveryError> {
205 if members.is_empty() {
206 return Err(DiscoveryError::EmptyFleet);
207 }
208
209 let mut canister_ids = BTreeSet::new();
210 for member in members {
211 if !canister_ids.insert(member.canister_id.clone()) {
212 return Err(DiscoveryError::DuplicateCanisterId(
213 member.canister_id.clone(),
214 ));
215 }
216 if member.verification_checks.is_empty() {
217 return Err(DiscoveryError::MissingVerificationChecks(
218 member.canister_id.clone(),
219 ));
220 }
221 }
222
223 Ok(())
224}