Skip to content

Commit 2b07e8e

Browse files
committed
feat: [#246] implement Phase 3 Grafana firewall configuration
This commit implements firewall configuration for Grafana UI access (port 3100), completing Phase 3 of the Grafana slice implementation. The firewall configuration follows the same pattern as tracker firewall with conditional execution based on Grafana configuration presence. ## Key Changes ### 1. Firewall Playbook (NEW) - Created `templates/ansible/configure-grafana-firewall.yml` - Opens port 3100 for Grafana UI (container port 3000 → host port 3100) - Unconditional execution when playbook runs (decision at step level) - Reloads UFW firewall after rule changes ### 2. Ansible Variables Context (UPDATED) - Added grafana_config parameter to `AnsibleVariablesContext::new()` - Marked as unused (`_grafana_config`) - for future use if needed - No grafana_enabled variable needed (conditional at step level) - Updated all call sites and tests (1555 tests passing) ### 3. Template Rendering (UPDATED) - Extended `RenderAnsibleTemplatesStep` with grafana_config field - Updated constructor and execute() to pass grafana_config to renderer - Updated `AnsibleProjectGenerator::render()` with grafana_config param - Updated `AnsibleTemplateService` to pass grafana from user_inputs ### 4. Ansible Project Generator (UPDATED) - Registered `configure-grafana-firewall.yml` in `copy_static_templates()` - Updated file count comment: 17 files (ansible.cfg + 16 playbooks) - Playbook placed after `configure-tracker-firewall.yml` in list ### 5. Configure Command (UPDATED) - Added `ConfigureGrafanaFirewall` variant to `ConfigureStep` enum - Created `ConfigureGrafanaFirewallStep` following tracker firewall pattern - Integrated in `ConfigureCommandHandler` after tracker firewall step - Conditional execution: - Skip if `TORRUST_TD_SKIP_FIREWALL_IN_CONTAINER=true` - Skip if Grafana not configured (check `context().user_inputs.grafana`) - Execute only when Grafana is enabled in environment ## Design Decisions ### Pattern Choice: Step-Level Conditional Execution Unlike tracker firewall (which uses variable-based conditionals for port arrays), Grafana firewall uses **step-level conditional execution** because: 1. Grafana UI port is fixed (3100), not variable like tracker ports 2. Simpler to check presence of Grafana config at step level 3. Follows same pattern as Prometheus (no public firewall exposure) 4. Playbook always opens port 3100 when executed - simple & clear ### Why No `grafana_enabled` Variable? Initial implementation added `grafana_enabled` to variables.yml.tera, but this was removed because: 1. Tracker uses `when: tracker_udp_ports is defined` for conditionals 2. Grafana doesn't need variable-based conditionals (port is fixed) 3. Decision happens at step level: don't execute playbook if Grafana disabled 4. Simpler pattern: playbook unconditionally opens port when run ## Security Note This public port exposure is **temporary** until HTTPS support with reverse proxy is implemented. Once nginx + HTTPS is added, Grafana will only be accessible through the proxy. ## Testing - ✅ All 1555 unit tests passing - ✅ Pre-commit checks passing (4m 28s) - cargo machete (no unused dependencies) - All linters passing (markdown, yaml, toml, cspell, clippy, rustfmt, shellcheck) - E2E infrastructure lifecycle tests (55s) - E2E deployment workflow tests (1m 29s) ## Next Steps (Phase 3 - Testing & Verification) - [ ] Create E2E test configurations with Grafana enabled/disabled - [ ] Extend E2E validators to verify Grafana deployment and firewall - [ ] Test validation error (Grafana without Prometheus) - [ ] Run manual E2E test with Grafana enabled ## Files Changed - `src/application/steps/system/configure_grafana_firewall.rs` (NEW) - `templates/ansible/configure-grafana-firewall.yml` (NEW) - `src/application/command_handlers/configure/handler.rs` (UPDATED) - `src/application/services/ansible_template_service.rs` (UPDATED) - `src/application/steps/rendering/ansible_templates.rs` (UPDATED) - `src/application/steps/system/mod.rs` (UPDATED) - `src/domain/environment/state/configure_failed.rs` (UPDATED) - `src/infrastructure/templating/ansible/**` (UPDATED - variables context) - `docs/issues/246-grafana-slice-release-run-commands.md` (UPDATED) Related: #246
1 parent a04b4bc commit 2b07e8e

File tree

13 files changed

+278
-43
lines changed

13 files changed

+278
-43
lines changed

docs/issues/246-grafana-slice-release-run-commands.md

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -593,30 +593,30 @@ fn create_environment_from_config(config: UserInputs) -> Result<Environment, Con
593593

594594
6. **Firewall Configuration** (NEW):
595595

596-
- [ ] Create Ansible playbook: `templates/ansible/configure-grafana-firewall.yml.tera`
597-
- [ ] Add `grafana_enabled` variable to Ansible variables template
598-
- [ ] Register playbook in `ProjectGenerator` (see `templates.md` for static template registration)
599-
- [ ] Create `ConfigureGrafanaFirewallStep` in `src/application/steps/configure_grafana_firewall.rs`:
596+
- [x] Create Ansible playbook: `templates/ansible/configure-grafana-firewall.yml`
597+
- [x] ~~Add `grafana_enabled` variable to Ansible variables template~~ (NOT NEEDED - conditional at step level)
598+
- [x] Register playbook in `ProjectGenerator` (see `templates.md` for static template registration)
599+
- [x] Create `ConfigureGrafanaFirewallStep` in `src/application/steps/system/configure_grafana_firewall.rs`:
600600
- Implement `Step` trait with `execute()` method
601601
- Execute `configure-grafana-firewall.yml` playbook via Ansible client
602602
- Return appropriate error on failure
603-
- [ ] Add `ConfigureGrafanaFirewall` variant to `ConfigureStep` enum in `src/domain/environment/state.rs`
604-
- [ ] Integrate step in `ConfigureCommandHandler::execute_configuration_with_tracking()`:
603+
- [x] Add `ConfigureGrafanaFirewall` variant to `ConfigureStep` enum in `src/domain/environment/state/configure_failed.rs`
604+
- [x] Integrate step in `ConfigureCommandHandler::execute_configuration_with_tracking()`:
605605
- Add after `ConfigureTrackerFirewall` step
606606
- Check `skip_firewall` flag (respect `TORRUST_TD_SKIP_FIREWALL_IN_CONTAINER`)
607607
- Skip with info log if firewall configuration is disabled
608-
- Execute `ConfigureGrafanaFirewallStep` otherwise
609-
- [ ] Add unit tests for `ConfigureGrafanaFirewallStep`
608+
- Execute `ConfigureGrafanaFirewallStep` only when Grafana is enabled
609+
- [x] Add unit tests for `ConfigureGrafanaFirewallStep`
610610

611611
7. **Testing**:
612-
- [ ] Add unit tests for Grafana service rendering in docker-compose template
613-
- [ ] Test conditional rendering (with/without Grafana)
614-
- [ ] Test environment variable generation
615-
- [ ] Test volume declaration
616-
- [ ] Test firewall configuration playbook rendering
617-
- [ ] Test `ConfigureGrafanaFirewallStep` execution
618-
- [ ] Run `cargo test` - all tests should pass (1500+ tests)
619-
- [ ] Run `cargo run --bin linter all` - all linters should pass
612+
- [x] Add unit tests for Grafana service rendering in docker-compose template
613+
- [x] Test conditional rendering (with/without Grafana)
614+
- [x] Test environment variable generation
615+
- [x] Test volume declaration
616+
- [x] Test firewall configuration playbook rendering
617+
- [x] Test `ConfigureGrafanaFirewallStep` execution
618+
- [x] Run `cargo test` - all tests should pass (1555 tests)
619+
- [x] Run `cargo run --bin linter all` - all linters should pass
620620

621621
### Phase 3: Testing & Verification
622622

src/application/command_handlers/configure/handler.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ use super::errors::ConfigureCommandHandlerError;
88
use crate::adapters::ansible::AnsibleClient;
99
use crate::application::command_handlers::common::StepResult;
1010
use crate::application::steps::{
11-
ConfigureFirewallStep, ConfigureSecurityUpdatesStep, ConfigureTrackerFirewallStep,
12-
InstallDockerComposeStep, InstallDockerStep,
11+
ConfigureFirewallStep, ConfigureGrafanaFirewallStep, ConfigureSecurityUpdatesStep,
12+
ConfigureTrackerFirewallStep, InstallDockerComposeStep, InstallDockerStep,
1313
};
1414
use crate::domain::environment::repository::{EnvironmentRepository, TypedEnvironmentRepository};
1515
use crate::domain::environment::state::{ConfigureFailureContext, ConfigureStep};
@@ -218,6 +218,34 @@ impl ConfigureCommandHandler {
218218
.map_err(|e| (e.into(), current_step))?;
219219
}
220220

221+
let current_step = ConfigureStep::ConfigureGrafanaFirewall;
222+
// Configure Grafana-specific firewall rules (conditional on Grafana configuration)
223+
// Only execute if Grafana is configured in the environment
224+
if skip_firewall {
225+
info!(
226+
command = "configure",
227+
step = "configure_grafana_firewall",
228+
status = "skipped",
229+
"Skipping Grafana firewall configuration due to TORRUST_TD_SKIP_FIREWALL_IN_CONTAINER"
230+
);
231+
} else if environment.context().user_inputs.grafana.is_some() {
232+
info!(
233+
command = "configure",
234+
step = "configure_grafana_firewall",
235+
"Configuring Grafana firewall (Grafana enabled)"
236+
);
237+
ConfigureGrafanaFirewallStep::new(Arc::clone(&ansible_client))
238+
.execute()
239+
.map_err(|e| (e.into(), current_step))?;
240+
} else {
241+
info!(
242+
command = "configure",
243+
step = "configure_grafana_firewall",
244+
status = "skipped",
245+
"Skipping Grafana firewall configuration (Grafana disabled)"
246+
);
247+
}
248+
221249
// Transition to Configured state
222250
let configured = environment.clone().configured();
223251

src/application/services/ansible_template_service.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ impl AnsibleTemplateService {
153153
user_inputs.ssh_credentials.clone(),
154154
ssh_socket_addr,
155155
user_inputs.tracker.clone(),
156+
user_inputs.grafana.clone(),
156157
)
157158
.execute()
158159
.await

src/application/steps/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ pub use rendering::{
3939
};
4040
pub use software::{InstallDockerComposeStep, InstallDockerStep};
4141
pub use system::{
42-
ConfigureFirewallStep, ConfigureSecurityUpdatesStep, ConfigureTrackerFirewallStep,
43-
WaitForCloudInitStep,
42+
ConfigureFirewallStep, ConfigureGrafanaFirewallStep, ConfigureSecurityUpdatesStep,
43+
ConfigureTrackerFirewallStep, WaitForCloudInitStep,
4444
};
4545
pub use validation::{
4646
ValidateCloudInitCompletionStep, ValidateDockerComposeInstallationStep,

src/application/steps/rendering/ansible_templates.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use thiserror::Error;
2525
use tracing::{info, instrument};
2626

2727
use crate::adapters::ssh::credentials::SshCredentials;
28+
use crate::domain::grafana::GrafanaConfig;
2829
use crate::domain::tracker::TrackerConfig;
2930
use crate::infrastructure::templating::ansible::template::renderer::AnsibleProjectGeneratorError;
3031
use crate::infrastructure::templating::ansible::template::wrappers::inventory::{
@@ -87,6 +88,7 @@ pub struct RenderAnsibleTemplatesStep {
8788
ssh_credentials: SshCredentials,
8889
ssh_socket_addr: SocketAddr,
8990
tracker_config: TrackerConfig,
91+
grafana_config: Option<GrafanaConfig>,
9092
}
9193

9294
impl RenderAnsibleTemplatesStep {
@@ -96,12 +98,14 @@ impl RenderAnsibleTemplatesStep {
9698
ssh_credentials: SshCredentials,
9799
ssh_socket_addr: SocketAddr,
98100
tracker_config: TrackerConfig,
101+
grafana_config: Option<GrafanaConfig>,
99102
) -> Self {
100103
Self {
101104
ansible_project_generator,
102105
ssh_credentials,
103106
ssh_socket_addr,
104107
tracker_config,
108+
grafana_config,
105109
}
106110
}
107111

@@ -127,7 +131,11 @@ impl RenderAnsibleTemplatesStep {
127131

128132
// Use the configuration renderer to handle all template rendering
129133
self.ansible_project_generator
130-
.render(&inventory_context, Some(&self.tracker_config))
134+
.render(
135+
&inventory_context,
136+
Some(&self.tracker_config),
137+
self.grafana_config.as_ref(),
138+
)
131139
.await?;
132140

133141
info!(
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
//! Grafana firewall configuration step
2+
//!
3+
//! This module provides the `ConfigureGrafanaFirewallStep` which handles configuration
4+
//! of UFW firewall rules for Grafana UI access. This step opens port 3100 to allow
5+
//! public access to the Grafana web interface for metrics visualization.
6+
//!
7+
//! ## Key Features
8+
//!
9+
//! - Opens firewall port 3100 for Grafana UI (container port 3000 → host port 3100)
10+
//! - Reloads firewall rules without disrupting SSH access
11+
//! - Conditional execution based on Grafana configuration presence
12+
//!
13+
//! ## Port Configuration
14+
//!
15+
//! The Grafana UI is exposed on a fixed port:
16+
//! - **Host port 3100** → Container port 3000 (Grafana default)
17+
//! - Unlike tracker ports, this is not configurable (fixed mapping)
18+
//!
19+
//! ## Execution Order
20+
//!
21+
//! This step must be run **AFTER** `ConfigureFirewallStep` (which sets up SSH access).
22+
//! It should only be executed if Grafana configuration is present in the environment.
23+
//!
24+
//! ## Security Note
25+
//!
26+
//! This public port exposure is **temporary** until HTTPS support with reverse proxy
27+
//! is implemented. Once a reverse proxy (like nginx) is added with HTTPS, this direct
28+
//! port exposure will be removed, and Grafana will only be accessible through the proxy.
29+
//!
30+
//! ## Safety
31+
//!
32+
//! This step is designed to be safe for the following reasons:
33+
//! 1. SSH firewall rules are already configured by `ConfigureFirewallStep`
34+
//! 2. Only opens a single, fixed port (3100)
35+
//! 3. Firewall reload preserves existing rules
36+
//! 4. No risk of SSH lockout (SSH rules already applied)
37+
38+
use std::sync::Arc;
39+
use tracing::{info, instrument};
40+
41+
use crate::adapters::ansible::AnsibleClient;
42+
use crate::shared::command::CommandError;
43+
44+
/// Step that configures UFW firewall rules for Grafana UI access
45+
///
46+
/// This step opens firewall port 3100 to allow public access to the Grafana
47+
/// web interface. The playbook execution is unconditional - the decision to
48+
/// execute this step is made at the command handler level based on whether
49+
/// Grafana is configured in the environment.
50+
pub struct ConfigureGrafanaFirewallStep {
51+
ansible_client: Arc<AnsibleClient>,
52+
}
53+
54+
impl ConfigureGrafanaFirewallStep {
55+
/// Create a new Grafana firewall configuration step
56+
///
57+
/// # Arguments
58+
///
59+
/// * `ansible_client` - Ansible client for running playbooks
60+
///
61+
/// # Note
62+
///
63+
/// Unlike tracker ports which are variable, Grafana UI port is fixed at 3100.
64+
/// The playbook always opens this port when executed - conditional execution
65+
/// happens at the step level (don't run if Grafana is disabled).
66+
#[must_use]
67+
pub fn new(ansible_client: Arc<AnsibleClient>) -> Self {
68+
Self { ansible_client }
69+
}
70+
71+
/// Execute the Grafana firewall configuration
72+
///
73+
/// This method opens firewall port 3100 for Grafana UI access and reloads
74+
/// the firewall. The port is fixed and not configurable.
75+
///
76+
/// # Safety
77+
///
78+
/// This method is designed to be safe because:
79+
/// - SSH firewall rules are already configured by `ConfigureFirewallStep`
80+
/// - Only opens a single, fixed port (3100)
81+
/// - Firewall reload preserves existing SSH rules
82+
///
83+
/// # Errors
84+
///
85+
/// Returns `CommandError` if:
86+
/// - Ansible playbook execution fails
87+
/// - UFW commands fail
88+
/// - Firewall reload fails
89+
#[instrument(
90+
name = "configure_grafana_firewall",
91+
skip_all,
92+
fields(
93+
step_type = "system",
94+
component = "firewall",
95+
service = "grafana",
96+
method = "ansible"
97+
)
98+
)]
99+
pub fn execute(&self) -> Result<(), CommandError> {
100+
info!(
101+
step = "configure_grafana_firewall",
102+
action = "open_grafana_ui_port",
103+
port = 3100,
104+
"Configuring UFW firewall for Grafana UI"
105+
);
106+
107+
// Run Ansible playbook
108+
// Unlike tracker firewall, no variables are needed (port is fixed at 3100)
109+
// The playbook unconditionally opens port 3100 when executed
110+
match self
111+
.ansible_client
112+
.run_playbook("configure-grafana-firewall", &["-e", "@variables.yml"])
113+
{
114+
Ok(_) => {
115+
info!(
116+
step = "configure_grafana_firewall",
117+
status = "success",
118+
port = 3100,
119+
"Grafana firewall rules configured successfully"
120+
);
121+
Ok(())
122+
}
123+
Err(e) => {
124+
// Propagate errors to the caller
125+
Err(e)
126+
}
127+
}
128+
}
129+
}
130+
131+
#[cfg(test)]
132+
mod tests {
133+
use std::path::PathBuf;
134+
use std::sync::Arc;
135+
136+
use super::*;
137+
138+
#[test]
139+
fn it_should_create_configure_grafana_firewall_step() {
140+
let ansible_client = Arc::new(AnsibleClient::new(PathBuf::from("test_inventory.yml")));
141+
let step = ConfigureGrafanaFirewallStep::new(ansible_client);
142+
143+
// Test that the step can be created successfully
144+
assert_eq!(
145+
std::mem::size_of_val(&step),
146+
std::mem::size_of::<Arc<AnsibleClient>>()
147+
);
148+
}
149+
}

src/application/steps/system/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* - Automatic security updates configuration
1010
* - UFW firewall configuration
1111
* - Tracker firewall configuration
12+
* - Grafana firewall configuration
1213
*
1314
* Future steps may include:
1415
* - User account setup and management
@@ -17,11 +18,13 @@
1718
*/
1819

1920
pub mod configure_firewall;
21+
pub mod configure_grafana_firewall;
2022
pub mod configure_security_updates;
2123
pub mod configure_tracker_firewall;
2224
pub mod wait_cloud_init;
2325

2426
pub use configure_firewall::ConfigureFirewallStep;
27+
pub use configure_grafana_firewall::ConfigureGrafanaFirewallStep;
2528
pub use configure_security_updates::ConfigureSecurityUpdatesStep;
2629
pub use configure_tracker_firewall::ConfigureTrackerFirewallStep;
2730
pub use wait_cloud_init::WaitForCloudInitStep;

src/domain/environment/state/configure_failed.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ pub enum ConfigureStep {
5151
ConfigureFirewall,
5252
/// Configuring Tracker firewall rules
5353
ConfigureTrackerFirewall,
54+
/// Configuring Grafana firewall rules
55+
ConfigureGrafanaFirewall,
5456
}
5557

5658
/// Error state - Application configuration failed

0 commit comments

Comments
 (0)