-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathhandler.rs
More file actions
247 lines (217 loc) · 9.25 KB
/
Copy pathhandler.rs
File metadata and controls
247 lines (217 loc) · 9.25 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
//! Configure command handler implementation
use std::sync::Arc;
use tracing::{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::domain::environment::repository::{EnvironmentRepository, TypedEnvironmentRepository};
use crate::domain::environment::state::{ConfigureFailureContext, ConfigureStep};
use crate::domain::environment::{Configured, Configuring, Environment, Provisioned};
use crate::infrastructure::trace::ConfigureTraceWriter;
use crate::shared::error::Traceable;
/// `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) ansible_client: Arc<AnsibleClient>,
pub(crate) clock: Arc<dyn crate::shared::Clock>,
pub(crate) repository: TypedEnvironmentRepository,
}
impl ConfigureCommandHandler {
/// Create a new `ConfigureCommandHandler`
#[must_use]
pub fn new(
ansible_client: Arc<AnsibleClient>,
clock: Arc<dyn crate::shared::Clock>,
repository: Arc<dyn EnvironmentRepository>,
) -> Self {
Self {
ansible_client,
clock,
repository: TypedEnvironmentRepository::new(repository),
}
}
/// Execute the complete configuration workflow
///
/// # Arguments
///
/// * `environment` - The environment in `Provisioned` state to configure
///
/// # Returns
///
/// Returns the configured environment
///
/// # Errors
///
/// Returns an error if any step in the configuration workflow fails:
/// * Docker installation fails
/// * Docker Compose installation fails
/// * Security updates configuration fails
///
/// On error, the environment transitions to `ConfigureFailed` state and is persisted.
#[instrument(
name = "configure_command",
skip_all,
fields(
command_type = "configure",
environment = %environment.name()
)
)]
pub fn execute(
&self,
environment: Environment<Provisioned>,
) -> Result<Environment<Configured>, ConfigureCommandHandlerError> {
info!(
command = "configure",
environment = %environment.name(),
"Starting complete infrastructure configuration workflow"
);
// Capture start time before transitioning to Configuring state
let started_at = self.clock.now();
// Transition to Configuring state
let environment = environment.start_configuring();
// Persist intermediate state
self.repository.save_configuring(&environment)?;
// Execute configuration steps with explicit step tracking
match self.execute_configuration_with_tracking(&environment) {
Ok(configured_env) => {
// Persist final state
self.repository.save_configured(&configured_env)?;
info!(
command = "configure",
environment = %configured_env.name(),
"Infrastructure configuration completed successfully"
);
Ok(configured_env)
}
Err((e, current_step)) => {
// Transition to error state with structured context
// current_step contains the step that was executing when the error occurred
let context =
self.build_failure_context(&environment, &e, current_step, started_at);
let failed = environment.configure_failed(context);
// Persist error state
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.
///
/// # Errors
///
/// Returns a tuple of (error, `current_step`) if any configuration step fails
fn execute_configuration_with_tracking(
&self,
environment: &Environment<Configuring>,
) -> StepResult<Environment<Configured>, ConfigureCommandHandlerError, ConfigureStep> {
// Track current step and execute each step
// If an error occurs, we return it along with the current step
let current_step = ConfigureStep::InstallDocker;
InstallDockerStep::new(Arc::clone(&self.ansible_client))
.execute()
.map_err(|e| (e.into(), current_step))?;
let current_step = ConfigureStep::InstallDockerCompose;
InstallDockerComposeStep::new(Arc::clone(&self.ansible_client))
.execute()
.map_err(|e| (e.into(), current_step))?;
let current_step = ConfigureStep::ConfigureSecurityUpdates;
ConfigureSecurityUpdatesStep::new(Arc::clone(&self.ansible_client))
.execute()
.map_err(|e| (e.into(), current_step))?;
let current_step = ConfigureStep::ConfigureFirewall;
// 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")
.map(|v| v == "true")
.unwrap_or(false);
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(&self.ansible_client))
.execute()
.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
}
}