Skip to main content

canic_backup/plan/
build.rs

1use super::{
2    BackupOperation, BackupOperationKind, BackupPlan, BackupPlanError, BackupScopeKind,
3    BackupTarget, ControlAuthority, ControlAuthoritySource, QuiescencePolicy,
4    SnapshotReadAuthority,
5};
6use crate::{
7    discovery::{SnapshotTarget, targets_from_registry},
8    manifest::IdentityMode,
9    registry::RegistryEntry,
10};
11use candid::Principal;
12use std::{
13    collections::{BTreeMap, BTreeSet},
14    str::FromStr,
15};
16
17///
18/// BackupPlanBuildInput
19///
20
21pub struct BackupPlanBuildInput<'a> {
22    pub plan_id: String,
23    pub run_id: String,
24    pub fleet: String,
25    pub network: String,
26    pub root_canister_id: String,
27    pub selected_canister_id: Option<String>,
28    pub selected_scope_kind: BackupScopeKind,
29    pub include_descendants: bool,
30    pub topology_hash_before_quiesce: String,
31    pub registry: &'a [RegistryEntry],
32    pub control_authority: ControlAuthority,
33    pub snapshot_read_authority: SnapshotReadAuthority,
34    pub quiescence_policy: QuiescencePolicy,
35    pub identity_mode: IdentityMode,
36}
37
38/// Build a validated backup plan from the live root registry projection.
39pub fn build_backup_plan(input: BackupPlanBuildInput<'_>) -> Result<BackupPlan, BackupPlanError> {
40    let snapshot_read_authority = input.snapshot_read_authority.clone();
41    let quiescence_policy = input.quiescence_policy.clone();
42    let root_included = input.selected_scope_kind == BackupScopeKind::MaintenanceRoot;
43    let selected_subtree_root = selected_subtree_root(&input)?;
44    let snapshot_targets = snapshot_targets(&input)?;
45    let target_depths = target_depths(input.registry);
46    let targets = snapshot_targets
47        .into_iter()
48        .map(|target| {
49            backup_target(
50                target,
51                &target_depths,
52                input.control_authority.clone(),
53                input.snapshot_read_authority.clone(),
54                input.identity_mode.clone(),
55            )
56        })
57        .collect::<Vec<_>>();
58    let phases = build_backup_phases(&targets);
59
60    let plan = BackupPlan {
61        plan_id: input.plan_id,
62        run_id: input.run_id,
63        fleet: input.fleet,
64        network: input.network,
65        root_canister_id: input.root_canister_id,
66        selected_subtree_root,
67        selected_scope_kind: input.selected_scope_kind,
68        include_descendants: input.include_descendants,
69        root_included,
70        requires_root_controller: input.control_authority.source
71            == ControlAuthoritySource::RootController,
72        snapshot_read_authority,
73        quiescence_policy,
74        topology_hash_before_quiesce: input.topology_hash_before_quiesce,
75        targets,
76        phases,
77    };
78    plan.validate()?;
79    Ok(plan)
80}
81
82/// Resolve an operator selector to one concrete live registry canister id.
83pub fn resolve_backup_selector(
84    registry: &[RegistryEntry],
85    selector: &str,
86) -> Result<String, BackupPlanError> {
87    validate_nonempty("selector", selector)?;
88    if Principal::from_str(selector).is_ok() {
89        return registry
90            .iter()
91            .find(|entry| entry.pid == selector)
92            .map(|entry| entry.pid.clone())
93            .ok_or_else(|| BackupPlanError::UnknownSelector(selector.to_string()));
94    }
95
96    let matches = registry
97        .iter()
98        .filter(|entry| entry.role.as_deref() == Some(selector))
99        .map(|entry| entry.pid.clone())
100        .collect::<Vec<_>>();
101    match matches.as_slice() {
102        [canister] => Ok(canister.clone()),
103        [] => Err(BackupPlanError::UnknownSelector(selector.to_string())),
104        _ => Err(BackupPlanError::AmbiguousSelector {
105            selector: selector.to_string(),
106            matches,
107        }),
108    }
109}
110
111fn selected_subtree_root(
112    input: &BackupPlanBuildInput<'_>,
113) -> Result<Option<String>, BackupPlanError> {
114    match input.selected_scope_kind {
115        BackupScopeKind::NonRootFleet => Ok(None),
116        BackupScopeKind::Member | BackupScopeKind::Subtree | BackupScopeKind::MaintenanceRoot => {
117            input
118                .selected_canister_id
119                .clone()
120                .ok_or(BackupPlanError::EmptyField("selected_canister_id"))
121                .map(Some)
122        }
123    }
124}
125
126fn snapshot_targets(
127    input: &BackupPlanBuildInput<'_>,
128) -> Result<Vec<SnapshotTarget>, BackupPlanError> {
129    match input.selected_scope_kind {
130        BackupScopeKind::Member | BackupScopeKind::Subtree | BackupScopeKind::MaintenanceRoot => {
131            let selected = input
132                .selected_canister_id
133                .as_deref()
134                .ok_or(BackupPlanError::EmptyField("selected_canister_id"))?;
135            let recursive = input.selected_scope_kind == BackupScopeKind::Subtree
136                || input.include_descendants
137                || input.selected_scope_kind == BackupScopeKind::MaintenanceRoot;
138            targets_from_registry(input.registry, selected, recursive)
139                .map_err(BackupPlanError::from)
140        }
141        BackupScopeKind::NonRootFleet => Ok(input
142            .registry
143            .iter()
144            .filter(|entry| entry.pid != input.root_canister_id)
145            .map(|entry| SnapshotTarget {
146                canister_id: entry.pid.clone(),
147                role: entry.role.clone(),
148                parent_canister_id: entry.parent_pid.clone(),
149                module_hash: entry.module_hash.clone(),
150            })
151            .collect()),
152    }
153}
154
155fn backup_target(
156    target: SnapshotTarget,
157    target_depths: &BTreeMap<String, u32>,
158    control_authority: ControlAuthority,
159    snapshot_read_authority: SnapshotReadAuthority,
160    identity_mode: IdentityMode,
161) -> BackupTarget {
162    BackupTarget {
163        depth: target_depths
164            .get(&target.canister_id)
165            .copied()
166            .unwrap_or_default(),
167        canister_id: target.canister_id,
168        role: target.role,
169        parent_canister_id: target.parent_canister_id,
170        control_authority,
171        snapshot_read_authority,
172        identity_mode,
173        expected_module_hash: target.module_hash,
174    }
175}
176
177fn target_depths(registry: &[RegistryEntry]) -> BTreeMap<String, u32> {
178    let parents = registry
179        .iter()
180        .map(|entry| (entry.pid.as_str(), entry.parent_pid.as_deref()))
181        .collect::<BTreeMap<_, _>>();
182    registry
183        .iter()
184        .map(|entry| {
185            (
186                entry.pid.clone(),
187                target_depth(entry.pid.as_str(), &parents),
188            )
189        })
190        .collect()
191}
192
193fn target_depth(canister_id: &str, parents: &BTreeMap<&str, Option<&str>>) -> u32 {
194    let mut depth = 0;
195    let mut current = canister_id;
196    let mut seen = BTreeSet::new();
197
198    while let Some(Some(parent)) = parents.get(current) {
199        if !seen.insert(current) {
200            break;
201        }
202        depth += 1;
203        current = parent;
204    }
205
206    depth
207}
208
209fn build_backup_phases(targets: &[BackupTarget]) -> Vec<BackupOperation> {
210    let mut phases = vec![
211        operation(
212            "validate-topology",
213            BackupOperationKind::ValidateTopology,
214            None,
215        ),
216        operation(
217            "validate-control-authority",
218            BackupOperationKind::ValidateControlAuthority,
219            None,
220        ),
221        operation(
222            "validate-snapshot-read-authority",
223            BackupOperationKind::ValidateSnapshotReadAuthority,
224            None,
225        ),
226        operation(
227            "validate-quiescence-policy",
228            BackupOperationKind::ValidateQuiescencePolicy,
229            None,
230        ),
231    ];
232
233    let mut top_down = targets.iter().collect::<Vec<_>>();
234    top_down.sort_by(|left, right| {
235        left.depth
236            .cmp(&right.depth)
237            .then_with(|| left.canister_id.cmp(&right.canister_id))
238    });
239    for target in &top_down {
240        phases.push(operation(
241            format!("stop-{}", target.canister_id),
242            BackupOperationKind::Stop,
243            Some(target.canister_id.clone()),
244        ));
245    }
246    for target in &top_down {
247        phases.push(operation(
248            format!("snapshot-{}", target.canister_id),
249            BackupOperationKind::CreateSnapshot,
250            Some(target.canister_id.clone()),
251        ));
252    }
253
254    let mut bottom_up = top_down;
255    bottom_up.reverse();
256    for target in &bottom_up {
257        phases.push(operation(
258            format!("start-{}", target.canister_id),
259            BackupOperationKind::Start,
260            Some(target.canister_id.clone()),
261        ));
262    }
263
264    for target in targets {
265        phases.push(operation(
266            format!("download-{}", target.canister_id),
267            BackupOperationKind::DownloadSnapshot,
268            Some(target.canister_id.clone()),
269        ));
270        phases.push(operation(
271            format!("verify-{}", target.canister_id),
272            BackupOperationKind::VerifyArtifact,
273            Some(target.canister_id.clone()),
274        ));
275    }
276    phases.push(operation(
277        "finalize-manifest",
278        BackupOperationKind::FinalizeManifest,
279        None,
280    ));
281
282    phases
283        .into_iter()
284        .enumerate()
285        .map(|(index, mut phase)| {
286            phase.order = u32::try_from(index).unwrap_or(u32::MAX);
287            phase
288        })
289        .collect()
290}
291
292fn operation(
293    operation_id: impl Into<String>,
294    kind: BackupOperationKind,
295    target_canister_id: Option<String>,
296) -> BackupOperation {
297    BackupOperation {
298        operation_id: operation_id.into(),
299        order: 0,
300        kind,
301        target_canister_id,
302    }
303}
304
305fn validate_nonempty(field: &'static str, value: &str) -> Result<(), BackupPlanError> {
306    if value.trim().is_empty() {
307        Err(BackupPlanError::EmptyField(field))
308    } else {
309        Ok(())
310    }
311}