Skip to main content

canic_backup/plan/
error.rs

1//! Module: plan::error
2//!
3//! Responsibility: report typed backup plan validation failures.
4//! Does not own: plan construction, preflight validation, or execution state.
5//! Boundary: shared error contract for plan builders and execution preflights.
6
7use crate::discovery::DiscoveryError;
8
9use thiserror::Error as ThisError;
10
11///
12/// BackupPlanError
13///
14/// Typed backup plan construction, validation, or preflight failure.
15/// Owned by backup planning and returned before invalid execution can start.
16///
17
18#[derive(Debug, ThisError)]
19pub enum BackupPlanError {
20    #[error("unsupported backup plan version {0}")]
21    UnsupportedVersion(u16),
22
23    #[error("field {0} must not be empty")]
24    EmptyField(&'static str),
25
26    #[error("field {field} must be a valid principal: {value}")]
27    InvalidPrincipal { field: &'static str, value: String },
28
29    #[error("field {field} must be a 64-character hex topology hash: {value}")]
30    InvalidTopologyHash { field: &'static str, value: String },
31
32    #[error("field {field} must be a unix timestamp marker: {value}")]
33    InvalidTimestamp { field: &'static str, value: String },
34
35    #[error("backup plan has no targets")]
36    EmptyTargets,
37
38    #[error("duplicate backup target {0}")]
39    DuplicateTarget(String),
40
41    #[error("backup plan has {actual} operations, expected {expected}")]
42    OperationCountMismatch { expected: usize, actual: usize },
43
44    #[error("backup plan operation {index} field {field} does not match its canonical projection")]
45    OperationProjectionMismatch { index: usize, field: &'static str },
46
47    #[error("normal backup scope must not include root")]
48    RootIncludedWithoutMaintenance,
49
50    #[error("maintenance root scope must include root")]
51    MaintenanceRootExcludesRoot,
52
53    #[error("selected scope root {0} is not present in plan targets")]
54    SelectedRootNotInTargets(String),
55
56    #[error("non-root-deployment scope must not declare a selected subtree root")]
57    NonRootDeploymentHasSelectedRoot,
58
59    #[error("backup target {canister_id} has a parent cycle")]
60    TargetParentCycle { canister_id: String },
61
62    #[error(
63        "backup target {canister_id} has depth {actual}, expected {expected} below parent {parent_canister_id}"
64    )]
65    TargetDepthMismatch {
66        canister_id: String,
67        parent_canister_id: String,
68        expected: u64,
69        actual: u32,
70    },
71
72    #[error("backup target {canister_id} is disconnected from expected root {expected_root}")]
73    TargetDisconnected {
74        canister_id: String,
75        expected_root: String,
76    },
77
78    #[error("selected backup root {selected_root} has selected parent {parent_canister_id}")]
79    SelectedRootHasInternalParent {
80        selected_root: String,
81        parent_canister_id: String,
82    },
83
84    #[error("target {0} has no proven control authority")]
85    UnprovenControlAuthority(String),
86
87    #[error("target {0} has no proven snapshot read authority")]
88    UnprovenTargetSnapshotReadAuthority(String),
89
90    #[error("target {0} must be controllable by root for this plan")]
91    MissingRootController(String),
92
93    #[error("target {0} has no control authority receipt")]
94    MissingControlAuthorityReceipt(String),
95
96    #[error("target {0} has no snapshot read authority receipt")]
97    MissingSnapshotReadAuthorityReceipt(String),
98
99    #[error("authority receipt targets unknown canister {0}")]
100    UnknownAuthorityReceiptTarget(String),
101
102    #[error("duplicate authority receipt for target {0}")]
103    DuplicateAuthorityReceipt(String),
104
105    #[error("authority receipt plan id {actual} does not match plan {expected}")]
106    AuthorityReceiptPlanMismatch { expected: String, actual: String },
107
108    #[error("authority receipt preflight id {actual} does not match preflight {expected}")]
109    AuthorityReceiptPreflightMismatch { expected: String, actual: String },
110
111    #[error("preflight receipt plan id {actual} does not match plan {expected}")]
112    PreflightReceiptPlanMismatch { expected: String, actual: String },
113
114    #[error("preflight receipt id {actual} does not match preflight {expected}")]
115    PreflightReceiptIdMismatch { expected: String, actual: String },
116
117    #[error(
118        "preflight receipt {preflight_id} is not valid yet at {as_of}; validated at {validated_at}"
119    )]
120    PreflightReceiptNotYetValid {
121        preflight_id: String,
122        validated_at: String,
123        as_of: String,
124    },
125
126    #[error("preflight receipt {preflight_id} expired at {expires_at}; checked at {as_of}")]
127    PreflightReceiptExpired {
128        preflight_id: String,
129        expires_at: String,
130        as_of: String,
131    },
132
133    #[error("preflight receipt {preflight_id} has invalid validity window")]
134    PreflightReceiptInvalidWindow { preflight_id: String },
135
136    #[error("topology preflight hash drifted from {expected} to {actual}")]
137    TopologyPreflightHashMismatch { expected: String, actual: String },
138
139    #[error("topology preflight targets do not match selected plan targets")]
140    TopologyPreflightTargetsMismatch,
141
142    #[error("quiescence preflight policy does not match plan")]
143    QuiescencePolicyMismatch,
144
145    #[error("quiescence preflight was not accepted")]
146    QuiescencePreflightRejected,
147
148    #[error("quiescence preflight targets do not match selected plan targets")]
149    QuiescencePreflightTargetsMismatch,
150
151    #[error("backup selector {0} did not match a live topology node")]
152    UnknownSelector(String),
153
154    #[error("backup selector {selector} matched multiple canisters: {matches:?}")]
155    AmbiguousSelector {
156        selector: String,
157        matches: Vec<String>,
158    },
159
160    #[error(transparent)]
161    Discovery(#[from] DiscoveryError),
162}