-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy patherrors.rs
More file actions
344 lines (323 loc) · 13.5 KB
/
Copy patherrors.rs
File metadata and controls
344 lines (323 loc) · 13.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
//! Error types for the Render command handler
use std::path::PathBuf;
use crate::domain::environment::name::EnvironmentName;
use crate::domain::environment::repository::RepositoryError;
use crate::shared::error::{ErrorKind, Traceable};
/// Comprehensive error type for the `RenderCommandHandler`
#[derive(Debug, thiserror::Error)]
pub enum RenderCommandHandlerError {
/// Environment was not found in repository (env-name mode)
#[error("Environment '{name}' not found")]
EnvironmentNotFound {
/// The name of the environment that was not found
name: EnvironmentName,
},
/// Environment is not in Created state (already provisioned)
///
/// Render command only works for Created environments (before provision)
#[error("Environment '{name}' is already in '{current_state}' state")]
EnvironmentAlreadyProvisioned {
/// The name of the environment
name: EnvironmentName,
/// The actual state of the environment
current_state: String,
},
/// Configuration file not found (env-file mode)
#[error("Configuration file not found: {path}")]
ConfigFileNotFound {
/// Path to the configuration file that was not found
path: PathBuf,
},
/// Configuration file parsing failed (env-file mode)
#[error("Failed to parse configuration file: {path}")]
ConfigParsingFailed {
/// Path to the configuration file
path: PathBuf,
/// JSON parsing error
#[source]
source: serde_json::Error,
},
/// Domain validation failed during config-to-params conversion
#[error("Configuration validation failed: {reason}")]
DomainValidationFailed {
/// Description of the validation failure
reason: String,
},
/// Invalid IP address format provided
#[error("Invalid IP address format: {value}")]
InvalidIpAddress {
/// The invalid IP address string
value: String,
},
/// Output directory already exists and force flag not provided
#[error("Output directory already exists: {path}")]
OutputDirectoryExists {
/// Path to the existing output directory
path: PathBuf,
},
/// Failed to create output directory
#[error("Failed to create output directory: {path}")]
OutputDirectoryCreationFailed {
/// Path to the output directory
path: PathBuf,
/// Reason for failure
reason: String,
},
/// Failed to render templates
#[error("Template rendering failed: {reason}")]
TemplateRenderingFailed {
/// Description of why rendering failed
reason: String,
},
/// Repository read error (env-name mode)
#[error("Failed to load environment: {0}")]
RepositoryLoad(#[from] RepositoryError),
/// No input mode specified (neither env-name nor env-file)
#[error("No input mode specified")]
NoInputMode,
}
impl Traceable for RenderCommandHandlerError {
fn trace_format(&self) -> String {
match self {
Self::EnvironmentNotFound { name } => {
format!("RenderCommandHandlerError: Environment '{name}' not found")
}
Self::EnvironmentAlreadyProvisioned {
name,
current_state,
} => {
format!(
"RenderCommandHandlerError: Environment '{name}' is in '{current_state}' state"
)
}
Self::ConfigFileNotFound { path } => {
format!(
"RenderCommandHandlerError: Configuration file not found: {}",
path.display()
)
}
Self::ConfigParsingFailed { path, source } => {
format!(
"RenderCommandHandlerError: Failed to parse {}: {source}",
path.display()
)
}
Self::DomainValidationFailed { reason } => {
format!("RenderCommandHandlerError: Configuration validation failed - {reason}")
}
Self::InvalidIpAddress { value } => {
format!("RenderCommandHandlerError: Invalid IP address: {value}")
}
Self::OutputDirectoryExists { path } => {
format!(
"RenderCommandHandlerError: Output directory already exists: {}",
path.display()
)
}
Self::OutputDirectoryCreationFailed { path, reason } => {
format!(
"RenderCommandHandlerError: Failed to create output directory {}: {reason}",
path.display()
)
}
Self::TemplateRenderingFailed { reason } => {
format!("RenderCommandHandlerError: Template rendering failed - {reason}")
}
Self::RepositoryLoad(e) => {
format!("RenderCommandHandlerError: Failed to load environment - {e}")
}
Self::NoInputMode => "RenderCommandHandlerError: No input mode specified".to_string(),
}
}
fn trace_source(&self) -> Option<&dyn Traceable> {
// RepositoryError doesn't implement Traceable, so we don't return sources
None
}
fn error_kind(&self) -> ErrorKind {
match self {
Self::EnvironmentNotFound { .. } | Self::ConfigFileNotFound { .. } => {
ErrorKind::FileSystem
}
Self::EnvironmentAlreadyProvisioned { .. }
| Self::ConfigParsingFailed { .. }
| Self::DomainValidationFailed { .. }
| Self::InvalidIpAddress { .. }
| Self::OutputDirectoryExists { .. }
| Self::OutputDirectoryCreationFailed { .. }
| Self::NoInputMode => ErrorKind::Configuration,
Self::TemplateRenderingFailed { .. } => ErrorKind::TemplateRendering,
Self::RepositoryLoad(_) => ErrorKind::StatePersistence,
}
}
}
impl RenderCommandHandlerError {
/// Provide user-friendly help text for errors
///
/// Each error variant includes actionable guidance on how to resolve
/// the issue or what the user should do next.
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn help(&self) -> String {
match self {
Self::EnvironmentNotFound { name } => {
format!(
"The environment '{name}' does not exist.\n\n\
To see available environments:\n \
torrust-tracker-deployer list\n\n\
To create this environment:\n \
torrust-tracker-deployer create environment --env-file <path>"
)
}
Self::EnvironmentAlreadyProvisioned {
name,
current_state,
} => {
format!(
"Environment '{name}' is in '{current_state}' state.\n\n\
The 'render' command only works for environments in 'Created' state\n\
(before provisioning infrastructure).\n\n\
For already provisioned environments, artifacts were generated during\n\
the provision command and are available at: build/{name}/\n\n\
If you need to regenerate artifacts with a different configuration:\n\
1. Create a new configuration file with your changes\n\
2. Use render in config-file mode:\n \
torrust-tracker-deployer render --env-file <path> --instance-ip <ip> --output-dir <dir>"
)
}
Self::ConfigFileNotFound { path } => {
format!(
"Configuration file not found: {}\n\n\
Make sure the file exists and the path is correct.\n\n\
To generate a configuration template:\n \
torrust-tracker-deployer create template --provider lxd",
path.display()
)
}
Self::ConfigParsingFailed { path, source } => {
format!(
"Failed to parse configuration file: {}\n\n\
JSON Error: {source}\n\n\
Make sure the file is valid JSON and follows the configuration schema.\n\n\
To validate your configuration:\n \
torrust-tracker-deployer validate --env-file {}",
path.display(),
path.display()
)
}
Self::DomainValidationFailed { reason } => {
format!(
"Configuration validation failed: {reason}\n\n\
Fix the configuration issues and try again.\n\n\
To validate your configuration:\n \
torrust-tracker-deployer validate --env-file <path>"
)
}
Self::InvalidIpAddress { value } => {
format!(
"Invalid IP address format: {value}\n\n\
Please provide a valid IPv4 or IPv6 address.\n\n\
Examples:\n \
IPv4: 10.0.0.1, 192.168.1.100\n \
IPv6: 2001:db8::1, ::1"
)
}
Self::OutputDirectoryExists { path } => {
format!(
"Output directory already exists: {}\n\n\
The output directory must not exist unless --force flag is provided.\n\n\
Options:\n\
1. Use a different output directory:\n \
torrust-tracker-deployer render ... --output-dir /path/to/new/directory\n\n\
2. Overwrite the existing directory (use with caution):\n \
torrust-tracker-deployer render ... --output-dir {} --force\n\n\
3. Remove the existing directory manually:\n \
rm -rf {}",
path.display(),
path.display(),
path.display()
)
}
Self::OutputDirectoryCreationFailed { path, reason } => {
format!(
"Failed to create output directory: {}\n\n\
Error: {reason}\n\n\
This may be due to:\n\
- Insufficient permissions\
- Invalid path\n\
- Filesystem errors\n\n\
Check that:\n\
- The parent directory exists and is writable\n\
- The path is valid for your operating system\n\
- You have the necessary permissions",
path.display()
)
}
Self::TemplateRenderingFailed { reason } => {
format!(
"Failed to render deployment artifacts: {reason}\n\n\
This is an internal error. Please report this issue with:\n\
- Your configuration file (redact sensitive data)\n\
- The full error message above\n\
- Environment details (OS, provider)"
)
}
Self::RepositoryLoad(e) => {
format!(
"Failed to load environment from repository.\n\n\
Repository error: {e}\n\n\
This may indicate data corruption or permission issues.\n\
Check that the data/ directory is readable and not corrupted."
)
}
Self::NoInputMode => {
"No input mode specified.\n\n\
You must provide either --env-name or --env-file.\n\n\
Examples:\n \
# From existing environment\n \
torrust-tracker-deployer render --env-name my-env --instance-ip 10.0.0.1\n\n \
# From configuration file\n \
torrust-tracker-deployer render --env-file envs/my-config.json --instance-ip 10.0.0.1"
.to_string()
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_should_implement_error_trait() {
let error = RenderCommandHandlerError::NoInputMode;
let _error_string: String = error.to_string();
}
#[test]
fn it_should_provide_help_text_for_all_variants() {
let errors = [
RenderCommandHandlerError::EnvironmentNotFound {
name: EnvironmentName::new("test").unwrap(),
},
RenderCommandHandlerError::EnvironmentAlreadyProvisioned {
name: EnvironmentName::new("test").unwrap(),
current_state: "Provisioned".to_string(),
},
RenderCommandHandlerError::ConfigFileNotFound {
path: PathBuf::from("test.json"),
},
RenderCommandHandlerError::InvalidIpAddress {
value: "not-an-ip".to_string(),
},
RenderCommandHandlerError::OutputDirectoryExists {
path: PathBuf::from("/tmp/output"),
},
RenderCommandHandlerError::OutputDirectoryCreationFailed {
path: PathBuf::from("/tmp/output"),
reason: "Permission denied".to_string(),
},
RenderCommandHandlerError::NoInputMode,
];
for error in &errors {
let help = error.help();
assert!(!help.is_empty(), "Help text should not be empty");
}
}
}