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("field {0} must not be empty")]
21    EmptyField(&'static str),
22
23    #[error("field {field} must be a valid principal: {value}")]
24    InvalidPrincipal { field: &'static str, value: String },
25
26    #[error("field {field} must be a 64-character hex topology hash: {value}")]
27    InvalidTopologyHash { field: &'static str, value: String },
28
29    #[error("field {field} must be a unix timestamp marker: {value}")]
30    InvalidTimestamp { field: &'static str, value: String },
31
32    #[error("backup plan has no targets")]
33    EmptyTargets,
34
35    #[error("backup plan has no phases")]
36    EmptyPhases,
37
38    #[error("duplicate backup target {0}")]
39    DuplicateTarget(String),
40
41    #[error("duplicate backup operation id {0}")]
42    DuplicateOperationId(String),
43
44    #[error("operation {operation_id} has order {order}, expected {expected}")]
45    OperationOrderMismatch {
46        operation_id: String,
47        order: u32,
48        expected: u32,
49    },
50
51    #[error("normal backup scope must not include root")]
52    RootIncludedWithoutMaintenance,
53
54    #[error("maintenance root scope must include root")]
55    MaintenanceRootExcludesRoot,
56
57    #[error("selected scope root {0} is not present in plan targets")]
58    SelectedRootNotInTargets(String),
59
60    #[error("non-root-deployment scope must not declare a selected subtree root")]
61    NonRootDeploymentHasSelectedRoot,
62
63    #[error("target {0} has no proven control authority")]
64    UnprovenControlAuthority(String),
65
66    #[error("target {0} has no proven snapshot read authority")]
67    UnprovenTargetSnapshotReadAuthority(String),
68
69    #[error("target {0} must be controllable by root for this plan")]
70    MissingRootController(String),
71
72    #[error("target {0} has no control authority receipt")]
73    MissingControlAuthorityReceipt(String),
74
75    #[error("target {0} has no snapshot read authority receipt")]
76    MissingSnapshotReadAuthorityReceipt(String),
77
78    #[error("authority receipt targets unknown canister {0}")]
79    UnknownAuthorityReceiptTarget(String),
80
81    #[error("duplicate authority receipt for target {0}")]
82    DuplicateAuthorityReceipt(String),
83
84    #[error("authority receipt plan id {actual} does not match plan {expected}")]
85    AuthorityReceiptPlanMismatch { expected: String, actual: String },
86
87    #[error("authority receipt preflight id {actual} does not match preflight {expected}")]
88    AuthorityReceiptPreflightMismatch { expected: String, actual: String },
89
90    #[error("preflight receipt plan id {actual} does not match plan {expected}")]
91    PreflightReceiptPlanMismatch { expected: String, actual: String },
92
93    #[error("preflight receipt id {actual} does not match preflight {expected}")]
94    PreflightReceiptIdMismatch { expected: String, actual: String },
95
96    #[error(
97        "preflight receipt {preflight_id} is not valid yet at {as_of}; validated at {validated_at}"
98    )]
99    PreflightReceiptNotYetValid {
100        preflight_id: String,
101        validated_at: String,
102        as_of: String,
103    },
104
105    #[error("preflight receipt {preflight_id} expired at {expires_at}; checked at {as_of}")]
106    PreflightReceiptExpired {
107        preflight_id: String,
108        expires_at: String,
109        as_of: String,
110    },
111
112    #[error("preflight receipt {preflight_id} has invalid validity window")]
113    PreflightReceiptInvalidWindow { preflight_id: String },
114
115    #[error("topology preflight hash drifted from {expected} to {actual}")]
116    TopologyPreflightHashMismatch { expected: String, actual: String },
117
118    #[error("topology preflight targets do not match selected plan targets")]
119    TopologyPreflightTargetsMismatch,
120
121    #[error("quiescence preflight policy does not match plan")]
122    QuiescencePolicyMismatch,
123
124    #[error("quiescence preflight was not accepted")]
125    QuiescencePreflightRejected,
126
127    #[error("quiescence preflight targets do not match selected plan targets")]
128    QuiescencePreflightTargetsMismatch,
129
130    #[error("operation {operation_id} targets unknown canister {target_canister_id}")]
131    UnknownOperationTarget {
132        operation_id: String,
133        target_canister_id: String,
134    },
135
136    #[error("backup selector {0} did not match a live topology node")]
137    UnknownSelector(String),
138
139    #[error("backup selector {selector} matched multiple canisters: {matches:?}")]
140    AmbiguousSelector {
141        selector: String,
142        matches: Vec<String>,
143    },
144
145    #[error("required preflight operation {0} is missing")]
146    MissingPreflight(&'static str),
147
148    #[error("mutating operation {operation_id} appears before required preflights")]
149    MutationBeforePreflight { operation_id: String },
150
151    #[error(transparent)]
152    Discovery(#[from] DiscoveryError),
153}