canic_backup/discovery/
mod.rs1use 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#[derive(Clone, Debug)]
16pub struct DiscoveredFleet {
17 pub topology_records: Vec<TopologyRecord>,
18 pub members: Vec<DiscoveredMember>,
19}
20
21impl DiscoveredFleet {
22 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#[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 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#[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#[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#[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
131pub 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
179fn 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;