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}
91
92impl Display for FleetConfigNameIssue {
93 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
94 formatter.write_str(match self {
95 Self::Empty => "must not be empty",
96 Self::InvalidCharacters => "must contain only ASCII letters, numbers, '_' or '-'",
97 })
98 }
99}
100
101#[derive(Clone, Debug, Eq, PartialEq)]
108pub enum FleetConfigDeclaration {
109 FleetName,
110 Role { fleet: String, role: String },
111}
112
113impl Display for FleetConfigDeclaration {
114 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
115 match self {
116 Self::FleetName => formatter.write_str("missing required [fleet].name in canic.toml"),
117 Self::Role { fleet, role } => write!(formatter, "role {fleet}.{role} is not declared"),
118 }
119 }
120}
121
122#[derive(Clone, Debug, Eq, PartialEq)]
129pub enum FleetConfigMutationConflict {
130 RoleAlreadyAttached { fleet: String, role: String },
131 RoleAlreadyDeclared { fleet: String, role: String },
132 RootRoleAttach,
133 RootRoleDeclare,
134 RootRoleRename,
135 SameRoleRename,
136}
137
138impl Display for FleetConfigMutationConflict {
139 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
140 match self {
141 Self::RoleAlreadyAttached { fleet, role } => {
142 write!(formatter, "role {fleet}.{role} is already attached")
143 }
144 Self::RoleAlreadyDeclared { fleet, role } => {
145 write!(formatter, "role {fleet}.{role} is already declared")
146 }
147 Self::RootRoleAttach => {
148 formatter.write_str("root role must already be attached through root topology")
149 }
150 Self::RootRoleDeclare => formatter
151 .write_str("root role must be attached to topology; declare ordinary roles only"),
152 Self::RootRoleRename => {
153 formatter.write_str("root role cannot be renamed through fleet role rename")
154 }
155 Self::SameRoleRename => formatter.write_str("old role and new role must differ"),
156 }
157 }
158}
159
160#[derive(Clone, Debug, Eq, PartialEq)]
167pub enum FleetConfigPackageIssue {
168 MetadataMissing,
169 MetadataMismatch {
170 expected_fleet: String,
171 expected_role: String,
172 },
173}
174
175impl Display for FleetConfigPackageIssue {
176 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
177 match self {
178 Self::MetadataMissing => {
179 formatter.write_str("updated manifest would remove [package.metadata.canic]")
180 }
181 Self::MetadataMismatch {
182 expected_fleet,
183 expected_role,
184 } => write!(
185 formatter,
186 "updated manifest would not contain expected Canic metadata fleet={expected_fleet:?} role={expected_role:?}"
187 ),
188 }
189 }
190}
191
192#[derive(Clone, Copy, Debug, Eq, PartialEq)]
199pub enum FleetConfigTomlOperation {
200 ParseFleetIdentity,
201 ParsePackageManifest,
202}
203
204#[derive(Debug, ThisError)]
211pub enum FleetConfigError {
212 #[error("invalid {}: {source}", path.display())]
213 ConfigInvalid {
214 path: PathBuf,
215 #[source]
216 source: Box<Self>,
217 },
218
219 #[error("{source}")]
220 CoreConfig {
221 operation: FleetConfigOperation,
222 #[source]
223 source: ConfigError,
224 },
225
226 #[error("{declaration}")]
227 DeclarationMissing { declaration: FleetConfigDeclaration },
228
229 #[error("selected config declares fleet {actual:?}, not {expected:?}")]
230 FleetMismatch { actual: String, expected: String },
231
232 #[error("kind must be one of: service, singleton, shard, replica, instance")]
233 InvalidKind { kind: String },
234
235 #[error("{field} {issue}")]
236 InvalidName {
237 field: FleetConfigNameField,
238 issue: FleetConfigNameIssue,
239 value: String,
240 },
241
242 #[error("{detail}")]
243 InvalidTableHeader { detail: &'static str },
244
245 #[error("failed to {operation} {}: {source}", path.display())]
246 Io {
247 operation: FleetConfigIoOperation,
248 path: PathBuf,
249 #[source]
250 source: io::Error,
251 },
252
253 #[error("{conflict}")]
254 MutationConflict {
255 conflict: FleetConfigMutationConflict,
256 },
257
258 #[error("updated {}: {issue}", path.display())]
259 PackageMetadataInvalid {
260 path: PathBuf,
261 issue: FleetConfigPackageIssue,
262 },
263
264 #[error("{}", format_role_contract_findings(.errors))]
265 RoleContractRejected { errors: Vec<RoleContractFinding> },
266
267 #[error("{mutation}; {rollback}")]
268 RollbackFailed {
269 mutation: Box<Self>,
270 rollback: Box<Self>,
271 },
272
273 #[error("{source}")]
274 Toml {
275 operation: FleetConfigTomlOperation,
276 #[source]
277 source: toml::de::Error,
278 },
279}
280
281impl FleetConfigError {
282 pub(super) fn at_config_path(self, path: &Path) -> Self {
283 match self {
284 Self::ConfigInvalid { .. } | Self::Io { .. } => self,
285 source => Self::ConfigInvalid {
286 path: path.to_path_buf(),
287 source: Box::new(source),
288 },
289 }
290 }
291
292 pub(super) fn io(operation: FleetConfigIoOperation, path: &Path, source: io::Error) -> Self {
293 Self::Io {
294 operation,
295 path: path.to_path_buf(),
296 source,
297 }
298 }
299}
300
301fn format_role_contract_findings(errors: &[RoleContractFinding]) -> String {
302 errors
303 .iter()
304 .map(|finding| {
305 format!(
306 "{}: {}",
307 finding.code(),
308 crate::role_contract::finding_detail(finding)
309 )
310 })
311 .collect::<Vec<_>>()
312 .join("; ")
313}