Skip to content

Commit a04b4bc

Browse files
committed
feat: [#246] implement Phase 2 Docker Compose integration for Grafana slice
This commit completes Phase 2 of the Grafana slice implementation, adding Docker Compose service configuration and template rendering support. Changes: **Docker Compose Integration:** - Extended DockerComposeContext with grafana_config field and with_grafana() builder - Extended EnvContext with GrafanaServiceConfig and with_grafana() method - Added conditional Grafana service to docker-compose.yml.tera template - Image: grafana/grafana:11.4.0 - Port mapping: 3100:3000 - Named volume: grafana_data - Depends on: prometheus - Added Grafana environment variables to .env.tera template - GF_SECURITY_ADMIN_USER - GF_SECURITY_ADMIN_PASSWORD **Environment Model:** - Added grafana_config() getter methods to Environment and EnvironmentContext - Re-exported GrafanaConfig from domain::environment module **Rendering Step:** - Extended RenderDockerComposeTemplatesStep with apply_grafana_config() method - Extended with apply_grafana_env_context() to expose secrets for templates - Properly exposes Password secrets for Tera template rendering **Code Quality:** - Refactored long namespace paths to use proper imports at module top - All 1554 unit tests passing - E2E infrastructure and deployment tests passing **Issue Progress:** - Updated issue checklist marking Phase 1 and Phase 2 tasks complete - Phase 3 (Firewall & Testing) remains pending Phase 2 follows the established pattern from Prometheus slice implementation and maintains consistency with the project's architecture and conventions.
1 parent 426f64a commit a04b4bc

File tree

10 files changed

+201
-47
lines changed

10 files changed

+201
-47
lines changed

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

Lines changed: 39 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -507,37 +507,37 @@ fn create_environment_from_config(config: UserInputs) -> Result<Environment, Con
507507

508508
1. **Domain Layer** (`src/domain/grafana/`):
509509

510-
- [ ] Create `src/domain/grafana/mod.rs` module
511-
- [ ] Create `src/domain/grafana/config.rs` with `GrafanaConfig` struct
512-
- [ ] Add `admin_user` and `admin_password` fields (both String)
513-
- [ ] Implement `Default` trait with default values ("admin"/"admin")
514-
- [ ] Add `Serialize`, `Deserialize`, `Debug`, `Clone`, `PartialEq` derives
515-
- [ ] Add comprehensive unit tests (5+ tests covering defaults, serialization, deserialization)
510+
- [x] Create `src/domain/grafana/mod.rs` module
511+
- [x] Create `src/domain/grafana/config.rs` with `GrafanaConfig` struct
512+
- [x] Add `admin_user` and `admin_password` fields (both String)
513+
- [x] Implement `Default` trait with default values ("admin"/"admin")
514+
- [x] Add `Serialize`, `Deserialize`, `Debug`, `Clone`, `PartialEq` derives
515+
- [x] Add comprehensive unit tests (5+ tests covering defaults, serialization, deserialization)
516516

517517
2. **Environment User Inputs Extension**:
518518

519-
- [ ] Add `grafana: Option<GrafanaConfig>` field to `UserInputs` struct
520-
- [ ] Add `#[serde(skip_serializing_if = "Option::is_none")]` attribute
521-
- [ ] Update all constructors and test fixtures to include `grafana` field
522-
- [ ] Update JSON schema (`schemas/environment-config.json`) with Grafana section
519+
- [x] Add `grafana: Option<GrafanaConfig>` field to `UserInputs` struct
520+
- [x] Add `#[serde(skip_serializing_if = "Option::is_none")]` attribute
521+
- [x] Update all constructors and test fixtures to include `grafana` field
522+
- [x] Update JSON schema (`schemas/environment-config.json`) with Grafana section
523523

524524
3. **Validation Logic** (`src/application/command_handlers/create/config/validation/`):
525525

526-
- [ ] Create validation module if it doesn't exist
527-
- [ ] Implement `validate_grafana_prometheus_dependency()` function
528-
- [ ] Add `ConfigError::GrafanaRequiresPrometheus` error variant
529-
- [ ] Add comprehensive error help text with fix instructions
530-
- [ ] Write unit tests for all validation scenarios:
531-
- [ ] Both enabled (valid)
532-
- [ ] Both disabled (valid)
533-
- [ ] Only Prometheus enabled (valid)
534-
- [ ] Only Grafana enabled (invalid - should error)
535-
- [ ] Integrate validation call in environment creation handler
536-
- [ ] Run linters and tests
526+
- [x] Create validation module if it doesn't exist
527+
- [x] Implement `validate_grafana_prometheus_dependency()` function
528+
- [x] Add `ConfigError::GrafanaRequiresPrometheus` error variant
529+
- [x] Add comprehensive error help text with fix instructions
530+
- [x] Write unit tests for all validation scenarios:
531+
- [x] Both enabled (valid)
532+
- [x] Both disabled (valid)
533+
- [x] Only Prometheus enabled (valid)
534+
- [x] Only Grafana enabled (invalid - should error)
535+
- [x] Integrate validation call in environment creation handler
536+
- [x] Run linters and tests
537537

538538
4. **Testing**:
539-
- [ ] Run `cargo test` - all tests should pass
540-
- [ ] Run `cargo run --bin linter all` - all linters should pass
539+
- [x] Run `cargo test` - all tests should pass
540+
- [x] Run `cargo run --bin linter all` - all linters should pass
541541

542542
### Phase 2: Docker Compose Integration
543543

@@ -547,26 +547,26 @@ fn create_environment_from_config(config: UserInputs) -> Result<Environment, Con
547547

548548
1. **Docker Compose Context** (`src/infrastructure/templating/docker_compose/template/wrappers/compose/context.rs`):
549549

550-
- [ ] Add `grafana_config: Option<GrafanaConfig>` field to `DockerComposeContext`
551-
- [ ] Implement `with_grafana()` method for context builder pattern
552-
- [ ] Add unit tests for Grafana context inclusion
550+
- [x] Add `grafana_config: Option<GrafanaConfig>` field to `DockerComposeContext`
551+
- [x] Implement `with_grafana()` method for context builder pattern
552+
- [x] Add unit tests for Grafana context inclusion
553553

554554
2. **Environment Variables Context** (`src/infrastructure/templating/docker_compose/template/wrappers/env/context.rs`):
555555

556-
- [ ] Add optional Grafana fields to `EnvContext` struct:
556+
- [x] Add optional Grafana fields to `EnvContext` struct:
557557
- `grafana_admin_user: Option<String>`
558558
- `grafana_admin_password: Option<String>` (plain String for template rendering)
559-
- [ ] Implement `new_with_grafana()` constructor method
560-
- [ ] Constructor must call `.expose_secret()` on `Password` to extract plaintext for template
561-
- [ ] Add getters for Grafana fields
562-
- [ ] Add unit tests for environment variable generation
559+
- [x] Implement `new_with_grafana()` constructor method
560+
- [x] Constructor must call `.expose_secret()` on `Password` to extract plaintext for template
561+
- [x] Add getters for Grafana fields
562+
- [x] Add unit tests for environment variable generation
563563

564564
**Security Note**: The `admin_password` is stored as plain `String` in the context because Tera templates need the plaintext value. The `Password` wrapper is only used in the domain model and configuration. Call `.expose_secret()` when constructing the context from `GrafanaConfig`.
565565

566566
3. **Docker Compose Template** (`templates/docker-compose/docker-compose.yml.tera`):
567567

568-
- [ ] Add conditional Grafana service block with `{% if grafana_config %}`
569-
- [ ] Configure Grafana service:
568+
- [x] Add conditional Grafana service block with `{% if grafana_config %}`
569+
- [x] Configure Grafana service:
570570
- Image: `grafana/grafana:11.4.0`
571571
- Container name: `grafana`
572572
- Restart policy: `unless-stopped`
@@ -576,20 +576,20 @@ fn create_environment_from_config(config: UserInputs) -> Result<Environment, Con
576576
- Volume: `grafana_data:/var/lib/grafana`
577577
- Logging configuration (10m max-size, 10 max-file)
578578
- Depends on: `prometheus`
579-
- [ ] Add conditional volume declaration for `grafana_data`
579+
- [x] Add conditional volume declaration for `grafana_data`
580580

581581
4. **Environment Template** (`templates/docker-compose/.env.tera`):
582582

583-
- [ ] Add conditional Grafana section with `{% if grafana_config %}`
584-
- [ ] Add environment variables:
583+
- [x] Add conditional Grafana section with `{% if grafana_config %}`
584+
- [x] Add environment variables:
585585
- `GF_SECURITY_ADMIN_USER='{{ grafana_admin_user }}'`
586586
- `GF_SECURITY_ADMIN_PASSWORD='{{ grafana_admin_password }}'`
587587

588588
5. **Release Command Integration** (`src/application/command_handlers/release/`):
589589

590-
- [ ] Update docker-compose rendering step to include Grafana context
591-
- [ ] Pass Grafana config to `DockerComposeContext::with_grafana()` when present
592-
- [ ] Pass Grafana credentials to `EnvContext` when present
590+
- [x] Update docker-compose rendering step to include Grafana context
591+
- [x] Pass Grafana config to `DockerComposeContext::with_grafana()` when present
592+
- [x] Pass Grafana credentials to `EnvContext` when present
593593

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

src/application/steps/rendering/docker_compose_templates.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,14 @@ impl<S> RenderDockerComposeTemplatesStep<S> {
125125

126126
// Apply Prometheus configuration (independent of database choice)
127127
let builder = self.apply_prometheus_config(builder);
128+
129+
// Apply Grafana configuration (independent of database choice)
130+
let builder = self.apply_grafana_config(builder);
128131
let docker_compose_context = builder.build();
129132

133+
// Apply Grafana credentials to env context
134+
let env_context = self.apply_grafana_env_context(env_context);
135+
130136
let compose_build_dir = generator
131137
.render(&env_context, &docker_compose_context)
132138
.await?;
@@ -210,6 +216,28 @@ impl<S> RenderDockerComposeTemplatesStep<S> {
210216
}
211217
}
212218

219+
fn apply_grafana_config(
220+
&self,
221+
builder: DockerComposeContextBuilder,
222+
) -> DockerComposeContextBuilder {
223+
if let Some(grafana_config) = self.environment.grafana_config() {
224+
builder.with_grafana(grafana_config.clone())
225+
} else {
226+
builder
227+
}
228+
}
229+
230+
fn apply_grafana_env_context(&self, env_context: EnvContext) -> EnvContext {
231+
if let Some(grafana_config) = self.environment.grafana_config() {
232+
env_context.with_grafana(
233+
grafana_config.admin_user().to_string(),
234+
grafana_config.admin_password().expose_secret().to_string(),
235+
)
236+
} else {
237+
env_context
238+
}
239+
}
240+
213241
fn extract_tracker_ports(tracker_config: &TrackerConfig) -> (Vec<u16>, Vec<u16>, u16) {
214242
// Extract UDP tracker ports
215243
let udp_ports: Vec<u16> = tracker_config

src/domain/environment/context.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
3838
use crate::adapters::ssh::SshCredentials;
3939
use crate::domain::environment::{EnvironmentName, InternalConfig, RuntimeOutputs, UserInputs};
40+
use crate::domain::grafana::GrafanaConfig;
41+
use crate::domain::prometheus::PrometheusConfig;
4042
use crate::domain::provider::ProviderConfig;
4143
use serde::{Deserialize, Serialize};
4244
use std::path::PathBuf;
@@ -337,10 +339,16 @@ impl EnvironmentContext {
337339

338340
/// Returns the Prometheus configuration if enabled
339341
#[must_use]
340-
pub fn prometheus_config(&self) -> Option<&crate::domain::prometheus::PrometheusConfig> {
342+
pub fn prometheus_config(&self) -> Option<&PrometheusConfig> {
341343
self.user_inputs.prometheus.as_ref()
342344
}
343345

346+
/// Returns the Grafana configuration if enabled
347+
#[must_use]
348+
pub fn grafana_config(&self) -> Option<&GrafanaConfig> {
349+
self.user_inputs.grafana.as_ref()
350+
}
351+
344352
/// Returns the build directory
345353
#[must_use]
346354
pub fn build_dir(&self) -> &PathBuf {

src/domain/environment/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ pub use crate::domain::tracker::{
134134
// Re-export Prometheus types for convenience
135135
pub use crate::domain::prometheus::PrometheusConfig;
136136

137+
// Re-export Grafana types for convenience
138+
pub use crate::domain::grafana::GrafanaConfig;
139+
137140
use crate::adapters::ssh::SshCredentials;
138141
use crate::domain::provider::ProviderConfig;
139142
use crate::domain::{InstanceName, ProfileName};
@@ -448,6 +451,12 @@ impl<S> Environment<S> {
448451
self.context.prometheus_config()
449452
}
450453

454+
/// Returns the Grafana configuration if enabled
455+
#[must_use]
456+
pub fn grafana_config(&self) -> Option<&GrafanaConfig> {
457+
self.context.grafana_config()
458+
}
459+
451460
/// Returns the SSH username for this environment
452461
#[must_use]
453462
pub fn ssh_username(&self) -> &Username {

src/domain/environment/testing.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use super::*;
77
use crate::adapters::ssh::SshCredentials;
88
use crate::domain::grafana::GrafanaConfig;
9+
use crate::domain::prometheus::PrometheusConfig;
910
use crate::domain::provider::{LxdConfig, ProviderConfig};
1011
use crate::domain::tracker::TrackerConfig;
1112
use crate::domain::EnvironmentName;
@@ -39,7 +40,7 @@ pub struct EnvironmentTestBuilder {
3940
ssh_key_name: String,
4041
ssh_username: String,
4142
temp_dir: TempDir,
42-
prometheus_config: Option<crate::domain::prometheus::PrometheusConfig>,
43+
prometheus_config: Option<PrometheusConfig>,
4344
}
4445

4546
impl EnvironmentTestBuilder {
@@ -55,7 +56,7 @@ impl EnvironmentTestBuilder {
5556
ssh_key_name: "test_key".to_string(),
5657
ssh_username: "torrust".to_string(),
5758
temp_dir: TempDir::new().expect("Failed to create temp directory"),
58-
prometheus_config: Some(crate::domain::prometheus::PrometheusConfig::default()),
59+
prometheus_config: Some(PrometheusConfig::default()),
5960
}
6061
}
6162

@@ -82,10 +83,7 @@ impl EnvironmentTestBuilder {
8283

8384
/// Sets the Prometheus configuration
8485
#[must_use]
85-
pub fn with_prometheus_config(
86-
mut self,
87-
config: Option<crate::domain::prometheus::PrometheusConfig>,
88-
) -> Self {
86+
pub fn with_prometheus_config(mut self, config: Option<PrometheusConfig>) -> Self {
8987
self.prometheus_config = config;
9088
self
9189
}

src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/builder.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Builder for `DockerComposeContext`
22
33
// Internal crate
4+
use crate::domain::grafana::GrafanaConfig;
45
use crate::domain::prometheus::PrometheusConfig;
56

67
use super::database::{DatabaseConfig, MysqlSetupConfig, DRIVER_MYSQL, DRIVER_SQLITE};
@@ -14,6 +15,7 @@ pub struct DockerComposeContextBuilder {
1415
ports: TrackerPorts,
1516
database: DatabaseConfig,
1617
prometheus_config: Option<PrometheusConfig>,
18+
grafana_config: Option<GrafanaConfig>,
1719
}
1820

1921
impl DockerComposeContextBuilder {
@@ -26,6 +28,7 @@ impl DockerComposeContextBuilder {
2628
mysql: None,
2729
},
2830
prometheus_config: None,
31+
grafana_config: None,
2932
}
3033
}
3134

@@ -54,13 +57,25 @@ impl DockerComposeContextBuilder {
5457
self
5558
}
5659

60+
/// Adds Grafana configuration
61+
///
62+
/// # Arguments
63+
///
64+
/// * `grafana_config` - Grafana configuration
65+
#[must_use]
66+
pub fn with_grafana(mut self, grafana_config: GrafanaConfig) -> Self {
67+
self.grafana_config = Some(grafana_config);
68+
self
69+
}
70+
5771
/// Builds the `DockerComposeContext`
5872
#[must_use]
5973
pub fn build(self) -> DockerComposeContext {
6074
DockerComposeContext {
6175
database: self.database,
6276
ports: self.ports,
6377
prometheus_config: self.prometheus_config,
78+
grafana_config: self.grafana_config,
6479
}
6580
}
6681
}

src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use serde::Serialize;
88

99
// Internal crate
10+
use crate::domain::grafana::GrafanaConfig;
1011
use crate::domain::prometheus::PrometheusConfig;
1112

1213
// Submodules
@@ -31,6 +32,9 @@ pub struct DockerComposeContext {
3132
/// Prometheus configuration (optional)
3233
#[serde(skip_serializing_if = "Option::is_none")]
3334
pub prometheus_config: Option<PrometheusConfig>,
35+
/// Grafana configuration (optional)
36+
#[serde(skip_serializing_if = "Option::is_none")]
37+
pub grafana_config: Option<GrafanaConfig>,
3438
}
3539

3640
impl DockerComposeContext {
@@ -93,6 +97,12 @@ impl DockerComposeContext {
9397
pub fn prometheus_config(&self) -> Option<&PrometheusConfig> {
9498
self.prometheus_config.as_ref()
9599
}
100+
101+
/// Get the Grafana configuration if present
102+
#[must_use]
103+
pub fn grafana_config(&self) -> Option<&GrafanaConfig> {
104+
self.grafana_config.as_ref()
105+
}
96106
}
97107

98108
#[cfg(test)]

0 commit comments

Comments
 (0)