Skip to main content

canic_backup/discovery/
mod.rs

1use 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///
10/// DiscoveredFleet
11///
12
13#[derive(Clone, Debug)]
14pub struct DiscoveredFleet {
15    pub topology_records: Vec<TopologyRecord>,
16    pub members: Vec<DiscoveredMember>,
17}
18
19impl DiscoveredFleet {
20    /// Convert discovered topology and member policy into a manifest fleet section.
21    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///
43/// DiscoveredMember
44///
45
46#[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    /// Project this discovery member into the manifest restore contract.
60    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///
82/// SnapshotPlan
83///
84
85#[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///
96/// SnapshotTarget
97///
98
99#[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///
108/// DiscoveryError
109///
110
111#[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
129/// Resolve selected target and children from registry entries.
130pub 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
177// Validate discovery output before building a manifest projection.
178fn 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;