canic_host/release_set/config/
error.rs1use std::{
8 fmt::{self, Display},
9 io,
10 path::{Path, PathBuf},
11};
12
13use canic_core::{bootstrap::ConfigError, role_contract::RoleContractFinding};
14use thiserror::Error as ThisError;
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum FleetConfigOperation {
24 AttachRole,
25 DeclareRole,
26 Project,
27 RenameRole,
28}
29
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub enum FleetConfigIoOperation {
38 ReadConfig,
39 ReadPackageManifest,
40 RestoreConfig,
41 WriteConfig,
42 WritePackageManifest,
43}
44
45impl Display for FleetConfigIoOperation {
46 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
47 formatter.write_str(match self {
48 Self::ReadConfig => "read fleet config",
49 Self::ReadPackageManifest => "read package manifest",
50 Self::RestoreConfig => "restore fleet config",
51 Self::WriteConfig => "write fleet config",
52 Self::WritePackageManifest => "write package manifest",
53 })
54 }
55}
56
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
64pub enum FleetConfigNameField {
65 Package,
66 Role,
67 Subnet,
68}
69
70impl Display for FleetConfigNameField {
71 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
72 formatter.write_str(match self {
73 Self::Package => "package",
74 Self::Role => "role",
75 Self::Subnet => "subnet",
76 })
77 }
78}
79
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
87pub enum FleetConfigNameIssue {
88 Empty,
89 InvalidCharacters,
90 InvalidSnakeCase,
91 TooLong { max_bytes: usize },
92}
93
94impl Display for FleetConfigNameIssue {
95 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
96 match self {
97 Self::Empty => formatter.write_str("must not be empty"),
98 Self::InvalidCharacters => formatter.write_str(
99 "must begin with an ASCII letter, number, or '_' and contain only ASCII letters, numbers, '_' or '-'",
100 ),
101 Self::InvalidSnakeCase => formatter.write_str(
102 "must use lowercase snake_case beginning with an ASCII letter, with nonempty lowercase alphanumeric words separated by single '_' characters",
103 ),
104 Self::TooLong { max_bytes } => {
105 write!(formatter, "must not exceed {max_bytes} bytes")
106 }
107 }
108 }
109}
110
111#[derive(Clone, Debug, Eq, PartialEq)]
118pub enum FleetConfigDeclaration {
119 FleetName,
120 Role { fleet: String, role: String },
121}
122
123impl Display for FleetConfigDeclaration {
124 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
125 match self {
126 Self::FleetName => formatter.write_str("missing required [fleet].name in canic.toml"),
127 Self::Role { fleet, role } => write!(formatter, "role {fleet}.{role} is not declared"),
128 }
129 }
130}
131
132#[derive(Clone, Debug, Eq, PartialEq)]
139pub enum FleetConfigMutationConflict {
140 RoleAlreadyAttached { fleet: String, role: String },
141 RoleAlreadyDeclared { fleet: String, role: String },
142 RootRoleAttach,
143 RootRoleDeclare,
144 RootRoleRename,
145 SameRoleRename,
146}
147
148impl Display for FleetConfigMutationConflict {
149 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
150 match self {
151 Self::RoleAlreadyAttached { fleet, role } => {
152 write!(formatter, "role {fleet}.{role} is already attached")
153 }
154 Self::RoleAlreadyDeclared { fleet, role } => {
155 write!(formatter, "role {fleet}.{role} is already declared")
156 }
157 Self::RootRoleAttach => {
158 formatter.write_str("root role must already be attached through root topology")
159 }
160 Self::RootRoleDeclare => formatter
161 .write_str("root role must be attached to topology; declare ordinary roles only"),
162 Self::RootRoleRename => {
163 formatter.write_str("root role cannot be renamed through fleet role rename")
164 }
165 Self::SameRoleRename => formatter.write_str("old role and new role must differ"),
166 }
167 }
168}
169
170#[derive(Clone, Debug, Eq, PartialEq)]
177pub enum FleetConfigPackageIssue {
178 MetadataMissing,
179 MetadataMismatch {
180 expected_fleet: String,
181 expected_role: String,
182 },
183}
184
185impl Display for FleetConfigPackageIssue {
186 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
187 match self {
188 Self::MetadataMissing => {
189 formatter.write_str("updated manifest would remove [package.metadata.canic]")
190 }
191 Self::MetadataMismatch {
192 expected_fleet,
193 expected_role,
194 } => write!(
195 formatter,
196 "updated manifest would not contain expected Canic metadata fleet={expected_fleet:?} role={expected_role:?}"
197 ),
198 }
199 }
200}
201
202#[derive(Clone, Copy, Debug, Eq, PartialEq)]
209pub enum FleetConfigTomlOperation {
210 ParseFleetIdentity,
211 ParsePackageManifest,
212}
213
214#[derive(Debug, ThisError)]
221pub enum FleetConfigError {
222 #[error("invalid {}: {source}", path.display())]
223 ConfigInvalid {
224 path: PathBuf,
225 #[source]
226 source: Box<Self>,
227 },
228
229 #[error("{source}")]
230 CoreConfig {
231 operation: FleetConfigOperation,
232 #[source]
233 source: ConfigError,
234 },
235
236 #[error("{declaration}")]
237 DeclarationMissing { declaration: FleetConfigDeclaration },
238
239 #[error("selected config declares fleet {actual:?}, not {expected:?}")]
240 FleetMismatch { actual: String, expected: String },
241
242 #[error("kind must be one of: service, singleton, shard, replica, instance")]
243 InvalidKind { kind: String },
244
245 #[error("{field} {issue}")]
246 InvalidName {
247 field: FleetConfigNameField,
248 issue: FleetConfigNameIssue,
249 value: String,
250 },
251
252 #[error("{detail}")]
253 InvalidTableHeader { detail: &'static str },
254
255 #[error("failed to {operation} {}: {source}", path.display())]
256 Io {
257 operation: FleetConfigIoOperation,
258 path: PathBuf,
259 #[source]
260 source: io::Error,
261 },
262
263 #[error("{conflict}")]
264 MutationConflict {
265 conflict: FleetConfigMutationConflict,
266 },
267
268 #[error("updated {}: {issue}", path.display())]
269 PackageMetadataInvalid {
270 path: PathBuf,
271 issue: FleetConfigPackageIssue,
272 },
273
274 #[error("{}", format_role_contract_findings(.errors))]
275 RoleContractRejected { errors: Vec<RoleContractFinding> },
276
277 #[error("{mutation}; {rollback}")]
278 RollbackFailed {
279 mutation: Box<Self>,
280 rollback: Box<Self>,
281 },
282
283 #[error("{source}")]
284 Toml {
285 operation: FleetConfigTomlOperation,
286 #[source]
287 source: toml::de::Error,
288 },
289}
290
291impl FleetConfigError {
292 pub(super) fn at_config_path(self, path: &Path) -> Self {
293 match self {
294 Self::ConfigInvalid { .. } | Self::Io { .. } => self,
295 source => Self::ConfigInvalid {
296 path: path.to_path_buf(),
297 source: Box::new(source),
298 },
299 }
300 }
301
302 pub(super) fn io(operation: FleetConfigIoOperation, path: &Path, source: io::Error) -> Self {
303 Self::Io {
304 operation,
305 path: path.to_path_buf(),
306 source,
307 }
308 }
309}
310
311fn format_role_contract_findings(errors: &[RoleContractFinding]) -> String {
312 errors
313 .iter()
314 .map(|finding| {
315 format!(
316 "{}: {}",
317 finding.code(),
318 crate::role_contract::finding_detail(finding)
319 )
320 })
321 .collect::<Vec<_>>()
322 .join("; ")
323}