-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathhandler.rs
More file actions
337 lines (299 loc) · 12.7 KB
/
Copy pathhandler.rs
File metadata and controls
337 lines (299 loc) · 12.7 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
//! Configure command handler implementation
use std::sync::Arc;
use tracing::{error, info, instrument};
use super::errors::ConfigureCommandHandlerError;
use crate::adapters::ansible::AnsibleClient;
use crate::application::command_handlers::common::StepResult;
use crate::application::steps::{
ConfigureFirewallStep, ConfigureSecurityUpdatesStep, InstallDockerComposeStep,
InstallDockerStep,
};
use crate::application::traits::CommandProgressListener;
use crate::domain::environment::repository::{EnvironmentRepository, TypedEnvironmentRepository};
use crate::domain::environment::state::{ConfigureFailureContext, ConfigureStep};
use crate::domain::environment::{Configured, Configuring, Environment};
use crate::domain::EnvironmentName;
use crate::infrastructure::trace::ConfigureTraceWriter;
use crate::shared::error::Traceable;
/// Total number of steps in the configuration workflow.
///
/// This constant is used for progress reporting via `CommandProgressListener`
/// to display step progress like "[Step 1/4] Installing Docker...".
const TOTAL_CONFIGURE_STEPS: usize = 4;
/// `ConfigureCommandHandler` orchestrates the complete infrastructure configuration workflow
///
/// The `ConfigureCommandHandler` orchestrates the complete infrastructure configuration workflow.
///
/// This command handles all steps required to configure infrastructure:
/// 1. Install Docker
/// 2. Install Docker Compose
/// 3. Configure automatic security updates
/// 4. Configure UFW firewall
///
/// # State Management
///
/// The command integrates with the type-state pattern for environment lifecycle:
/// - Accepts `Environment<Provisioned>` as input
/// - Transitions to `Environment<Configuring>` at start
/// - Returns `Environment<Configured>` on success
/// - Transitions to `Environment<ConfigureFailed>` on error
///
/// State is persisted after each transition using the injected repository.
/// Persistence failures are logged but don't fail the command (state remains valid in memory).
pub struct ConfigureCommandHandler {
pub(crate) clock: Arc<dyn crate::shared::Clock>,
pub(crate) repository: TypedEnvironmentRepository,
}
impl ConfigureCommandHandler {
/// Create a new `ConfigureCommandHandler`
#[must_use]
pub fn new(
clock: Arc<dyn crate::shared::Clock>,
repository: Arc<dyn EnvironmentRepository>,
) -> Self {
Self {
clock,
repository: TypedEnvironmentRepository::new(repository),
}
}
/// Execute the complete configuration workflow
///
/// # Arguments
///
/// * `env_name` - The name of the environment to configure
/// * `listener` - Optional progress listener for reporting step-level progress.
/// When provided, the handler reports progress at each of the 4 configuration steps.
/// When `None`, the handler executes silently (backward compatible).
///
/// # Returns
///
/// Returns the configured environment
///
/// # Errors
///
/// Returns an error if any step in the configuration workflow fails:
/// * Environment not found or not in `Provisioned` state
/// * Docker installation fails
/// * Docker Compose installation fails
/// * Security updates configuration fails
/// * Firewall configuration fails
///
/// On error, the environment transitions to `ConfigureFailed` state and is persisted.
#[instrument(
name = "configure_command",
skip_all,
fields(
command_type = "configure",
environment = %env_name
)
)]
pub fn execute(
&self,
env_name: &EnvironmentName,
listener: Option<&dyn CommandProgressListener>,
) -> Result<Environment<Configured>, ConfigureCommandHandlerError> {
let environment = self.load_provisioned_environment(env_name)?;
let started_at = self.clock.now();
let environment = environment.start_configuring();
self.repository.save_configuring(&environment)?;
match Self::execute_configuration_with_tracking(&environment, listener) {
Ok(configured_env) => {
info!(
command = "configure",
environment = %configured_env.name(),
"Infrastructure configuration completed successfully"
);
self.repository.save_configured(&configured_env)?;
Ok(configured_env)
}
Err((e, current_step)) => {
error!(
command = "configure",
environment = %environment.name(),
failed_step = ?current_step,
error = %e,
"Infrastructure configuration failed"
);
let context =
self.build_failure_context(&environment, &e, current_step, started_at);
let failed = environment.configure_failed(context);
self.repository.save_configure_failed(&failed)?;
Err(e)
}
}
}
/// Execute the configuration steps with step tracking
///
/// This method executes all configuration steps while tracking which step is currently
/// being executed. If an error occurs, it returns both the error and the step that
/// was being executed, enabling accurate failure context generation.
///
/// # Arguments
///
/// * `environment` - The environment in Configuring state
/// * `listener` - Optional progress listener for step-level reporting
///
/// # Errors
///
/// Returns a tuple of (error, `current_step`) if any configuration step fails
fn execute_configuration_with_tracking(
environment: &Environment<Configuring>,
listener: Option<&dyn CommandProgressListener>,
) -> StepResult<Environment<Configured>, ConfigureCommandHandlerError, ConfigureStep> {
let ansible_client = Arc::new(AnsibleClient::new(environment.ansible_build_dir()));
// Allow tests or CI to skip Docker installation
// (useful for container-based tests where Docker is already installed via Dockerfile)
let skip_docker =
std::env::var("TORRUST_TD_SKIP_DOCKER_INSTALL_IN_CONTAINER").is_ok_and(|v| v == "true");
// Step 1/4: Install Docker
let current_step = ConfigureStep::InstallDocker;
Self::notify_step_started(listener, 1, "Installing Docker");
if skip_docker {
info!(
command = "configure",
step = "install_docker",
status = "skipped",
"Skipping Docker installation due to TORRUST_TD_SKIP_DOCKER_INSTALL_IN_CONTAINER (Docker pre-installed)"
);
} else {
InstallDockerStep::new(Arc::clone(&ansible_client))
.execute(listener)
.map_err(|e| (e.into(), current_step))?;
}
// Step 2/4: Install Docker Compose
let current_step = ConfigureStep::InstallDockerCompose;
Self::notify_step_started(listener, 2, "Installing Docker Compose");
if skip_docker {
info!(
command = "configure",
step = "install_docker_compose",
status = "skipped",
"Skipping Docker Compose installation due to TORRUST_TD_SKIP_DOCKER_INSTALL_IN_CONTAINER (Docker Compose pre-installed)"
);
} else {
InstallDockerComposeStep::new(Arc::clone(&ansible_client))
.execute(listener)
.map_err(|e| (e.into(), current_step))?;
}
// Step 3/4: Configure automatic security updates
let current_step = ConfigureStep::ConfigureSecurityUpdates;
Self::notify_step_started(listener, 3, "Configuring automatic security updates");
ConfigureSecurityUpdatesStep::new(Arc::clone(&ansible_client))
.execute(listener)
.map_err(|e| (e.into(), current_step))?;
// Step 4/4: Configure firewall (UFW)
let current_step = ConfigureStep::ConfigureFirewall;
Self::notify_step_started(listener, 4, "Configuring firewall (UFW)");
// Allow tests or CI to explicitly skip the firewall configuration step
// (useful for container-based test runs where iptables/ufw require
// elevated kernel capabilities not available in unprivileged containers).
let skip_firewall =
std::env::var("TORRUST_TD_SKIP_FIREWALL_IN_CONTAINER").is_ok_and(|v| v == "true");
if skip_firewall {
info!(
command = "configure",
step = "configure_firewall",
status = "skipped",
"Skipping UFW firewall configuration due to TORRUST_TD_SKIP_FIREWALL_IN_CONTAINER"
);
} else {
ConfigureFirewallStep::new(Arc::clone(&ansible_client))
.execute(listener)
.map_err(|e| (e.into(), current_step))?;
}
// Transition to Configured state
let configured = environment.clone().configured();
Ok(configured)
}
/// Build failure context for a configuration error and generate trace file
///
/// This helper method builds structured error context including the failed step,
/// error classification, timing information, and generates a trace file for
/// post-mortem analysis.
///
/// The trace file is written to `{environment.data_dir()}/traces/{trace_id}.txt`
/// and contains a formatted representation of the entire error chain.
///
/// # Arguments
///
/// * `environment` - The environment being configured (for trace directory path)
/// * `error` - The configuration error that occurred
/// * `current_step` - The step that was executing when the error occurred
/// * `started_at` - The timestamp when configuration execution started
///
/// # Returns
///
/// A structured `ConfigureFailureContext` with timing, error details, and trace file path
fn build_failure_context(
&self,
environment: &Environment<Configuring>,
error: &ConfigureCommandHandlerError,
current_step: ConfigureStep,
started_at: chrono::DateTime<chrono::Utc>,
) -> ConfigureFailureContext {
use crate::application::command_handlers::common::failure_context::build_base_failure_context;
// Step that failed is directly provided - no reverse engineering needed
let failed_step = current_step;
// Get error kind from the error itself (errors are self-describing)
let error_kind = error.error_kind();
// Build base failure context using common helper
let base = build_base_failure_context(&self.clock, started_at, error.to_string());
// Build handler-specific context
let mut context = ConfigureFailureContext {
failed_step,
error_kind,
base,
};
// Generate trace file (logging handled by trace writer)
let traces_dir = environment.traces_dir();
let trace_writer = ConfigureTraceWriter::new(traces_dir, Arc::clone(&self.clock));
if let Ok(trace_file_path) = trace_writer.write_trace(&context, error) {
context.base.trace_file_path = Some(trace_file_path);
}
context
}
/// Load environment from storage and validate it is in `Provisioned` state
///
/// # Errors
///
/// Returns an error if:
/// * Persistence error occurs during load
/// * Environment does not exist
/// * Environment is not in `Provisioned` state
fn load_provisioned_environment(
&self,
env_name: &EnvironmentName,
) -> Result<
crate::domain::environment::Environment<crate::domain::environment::Provisioned>,
ConfigureCommandHandlerError,
> {
let any_env = self
.repository
.inner()
.load(env_name)
.map_err(|e| ConfigureCommandHandlerError::StatePersistence(e.into()))?;
let any_env = any_env.ok_or_else(|| ConfigureCommandHandlerError::EnvironmentNotFound {
name: env_name.to_string(),
})?;
Ok(any_env.try_into_provisioned()?)
}
/// Notify progress listener that a step has started
///
/// Helper method to notify the listener when a configuration step begins.
/// If no listener is provided, this is a no-op.
///
/// # Arguments
///
/// * `listener` - Optional progress listener
/// * `step_number` - The current step number (1-based)
/// * `description` - User-facing description of the step
fn notify_step_started(
listener: Option<&dyn CommandProgressListener>,
step_number: usize,
description: &str,
) {
if let Some(l) = listener {
l.on_step_started(step_number, TOTAL_CONFIGURE_STEPS, description);
}
}
}