diff --git a/docs/issues/287-docker-compose-topology-refactoring-epic.md b/docs/issues/287-docker-compose-topology-refactoring-epic.md index 6002b61d..b0d9f07d 100644 --- a/docs/issues/287-docker-compose-topology-refactoring-epic.md +++ b/docs/issues/287-docker-compose-topology-refactoring-epic.md @@ -28,8 +28,8 @@ Implementation follows a 5-PR strategy: - [x] [#288](https://github.com/torrust/torrust-tracker-deployer/issues/288) - [ADR] Bind Mount Standardization for Docker Compose (ADR-01) - [x] [#290](https://github.com/torrust/torrust-tracker-deployer/issues/290) - [BUG] Remove invalid "Grafana without Prometheus" template branch (BUG-01) -- [ ] [#292](https://github.com/torrust/torrust-tracker-deployer/issues/292) - [Refactor] Phase 0: Convert volumes to bind mounts with domain type (P0.1, P0.2) -- [ ] #X - [Refactor] Phase 1: Create Network domain types (P1.1, P1.2) +- [x] [#292](https://github.com/torrust/torrust-tracker-deployer/issues/292) - [Refactor] Phase 0: Convert volumes to bind mounts with domain type (P0.1, P0.2) +- [x] [#294](https://github.com/torrust/torrust-tracker-deployer/issues/294) - [Refactor] Phase 1: Create Network domain types (P1.1, P1.2) - [ ] #X - [Refactor] Phase 2: Create DockerComposeTopology aggregate (P2.1, P2.2) (Tasks will be created and linked as work progresses) diff --git a/docs/issues/phase-1-network-domain-types.md b/docs/issues/phase-1-network-domain-types.md new file mode 100644 index 00000000..8cc5496d --- /dev/null +++ b/docs/issues/phase-1-network-domain-types.md @@ -0,0 +1,263 @@ +# [Refactor] Phase 1: Create Network Domain Types + +**Issue**: [#294](https://github.com/torrust/torrust-tracker-deployer/issues/294) +**Parent Epic**: [#287](https://github.com/torrust/torrust-tracker-deployer/issues/287) - Docker Compose Topology Domain Model Refactoring +**Related**: + +- [Refactoring Plan](../refactors/plans/docker-compose-topology-domain-model.md#phase-1-domain-network-types-core-infrastructure) +- [Phase 0 PR #293](https://github.com/torrust/torrust-tracker-deployer/pull/293) - Bind mount foundation + +## Overview + +This task creates domain types for Docker Compose networks and migrates service configurations from `Vec` to type-safe `Vec`. This is PR 4 in the 5-PR refactoring strategy. + +## Goals + +- [x] Create type-safe `Network` enum to eliminate string-based network references +- [x] Migrate all service configs to use domain network types +- [x] Establish foundation for Phase 2 (automatic network derivation) + +## Implementation + +**PR**: [#295](https://github.com/torrust/torrust-tracker-deployer/pull/295) +**Commit**: `11ed1b0e` + +### Changes Made + +- Created `Network` enum in `src/domain/topology/network.rs` with 4 variants: + - `Database` - for database services + - `Metrics` - for Prometheus/exporters + - `Visualization` - for Grafana + - `Proxy` - for Caddy reverse proxy +- Implemented `name()`, `driver()`, `all()`, `Display`, custom `Serialize` +- Migrated all 5 service configs to use `Vec`: + - `TrackerServiceConfig` + - `MysqlServiceConfig` + - `CaddyServiceConfig` + - `PrometheusServiceConfig` + - `GrafanaServiceConfig` +- Added comprehensive unit tests (16 for Network, plus per-service tests) +- All 1953 unit tests pass +- E2E infrastructure and deployment tests pass + +## 🏗️ Architecture Requirements + +**DDD Layer**: Domain +**Module Path**: `src/domain/deployment/topology/` +**Pattern**: Value Object (enum with behavior) + +### Module Structure Requirements + +- [ ] Create `network.rs` in `src/domain/deployment/topology/` +- [ ] Export via `mod.rs` in topology module +- [ ] Follow DDD layer separation (see [docs/codebase-architecture.md](../../codebase-architecture.md)) + +### Architectural Constraints + +- [ ] Domain types must be independent of infrastructure concerns +- [ ] Serialization uses network name strings for template compatibility +- [ ] Error handling follows project conventions (see [docs/contributing/error-handling.md](../../contributing/error-handling.md)) + +### Anti-Patterns to Avoid + +- ❌ Hardcoding network names in multiple places +- ❌ Infrastructure layer creating domain types +- ❌ Template logic computing network assignments + +## Specifications + +### P1.1: Network Domain Type + +Create a domain enum representing Docker Compose networks: + +```rust +// src/domain/deployment/topology/network.rs + +/// Docker Compose networks used for service isolation +/// +/// Each network serves a specific security purpose: +/// - Database: Isolates database access to only the tracker +/// - Metrics: Allows Prometheus to scrape tracker metrics +/// - Visualization: Allows Grafana to query Prometheus +/// - Proxy: Allows Caddy to reverse proxy to backend services +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] +pub enum Network { + /// Network for database access (Tracker ↔ MySQL) + Database, + /// Network for metrics scraping (Tracker ↔ Prometheus) + Metrics, + /// Network for visualization queries (Prometheus ↔ Grafana) + Visualization, + /// Network for TLS proxy (Caddy ↔ backend services) + Proxy, +} + +impl Network { + /// Returns the network name as used in docker-compose.yml + pub fn name(&self) -> &'static str { + match self { + Network::Database => "database_network", + Network::Metrics => "metrics_network", + Network::Visualization => "visualization_network", + Network::Proxy => "proxy_network", + } + } + + /// Returns the network driver (always "bridge" for now) + pub fn driver(&self) -> &'static str { + "bridge" + } +} + +impl std::fmt::Display for Network { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name()) + } +} +``` + +### P1.2: Migrate Service Configs + +Update service configuration structs to use `Vec` instead of `Vec`: + +```rust +use crate::domain::deployment::topology::Network; + +pub struct TrackerServiceConfig { + // ... + pub networks: Vec, +} +``` + +Network assignment rules (from domain analysis): + +| Service | Networks | Conditions | +| ---------- | ------------------------------ | ------------------------------------- | +| Tracker | `Database`, `Metrics`, `Proxy` | Based on MySQL, Prometheus, TLS flags | +| MySQL | `Database` | Always (when enabled) | +| Prometheus | `Metrics`, `Visualization` | Based on Grafana flag | +| Grafana | `Visualization`, `Proxy` | Based on TLS flag | +| Caddy | `Proxy` + others | Based on proxied services | + +### Template Integration + +Create serialization wrapper for network names: + +```rust +/// Wrapper for serializing Network to its name string for templates +#[derive(Serialize)] +#[serde(transparent)] +pub struct NetworkRef(String); + +impl From for NetworkRef { + fn from(net: Network) -> Self { + Self(net.name().to_string()) + } +} +``` + +Or implement custom `Serialize` to emit the network name directly. + +## Implementation Plan + +### Phase 1: Create Network Domain Type (P1.1) (~30 min) + +- [ ] Create `src/domain/deployment/topology/network.rs` +- [ ] Add `Network` enum with all four variants +- [ ] Implement `name()` method returning network names +- [ ] Implement `driver()` method returning "bridge" +- [ ] Implement `Display` trait for template rendering +- [ ] Add `Serialize` implementation (emit name string) +- [ ] Export from `src/domain/deployment/topology/mod.rs` +- [ ] Add unit tests for Network enum + +### Phase 2: Migrate Service Configs (P1.2) (~1-2 hours) + +- [ ] Create `NetworkRef` wrapper or custom serialization +- [ ] Update `TrackerServiceConfig` to use `Vec` +- [ ] Update `MysqlServiceConfig` to use `Vec` +- [ ] Update `PrometheusServiceConfig` to use `Vec` +- [ ] Update `GrafanaServiceConfig` to use `Vec` +- [ ] Update `CaddyServiceConfig` to use `Vec` +- [ ] Update network computation logic in each service config +- [ ] Verify template rendering produces identical output +- [ ] Add unit tests for network assignment rules + +### Phase 3: Verification (~30 min) + +- [ ] Run all unit tests +- [ ] Run E2E tests +- [ ] Verify docker-compose.yml output matches previous behavior +- [ ] Run linters and fix any issues + +## Acceptance Criteria + +> **Note for Contributors**: These criteria define what the PR reviewer will check. Use this as your pre-review checklist before submitting the PR to minimize back-and-forth iterations. + +**Quality Checks**: + +- [ ] Pre-commit checks pass: `./scripts/pre-commit.sh` + +**Task-Specific Criteria**: + +- [ ] `Network` enum exists with `Database`, `Metrics`, `Visualization`, `Proxy` variants +- [ ] All service configs use `Vec` instead of `Vec` +- [ ] Generated docker-compose.yml is identical to before refactoring +- [ ] Unit tests cover all network assignment rules from domain analysis +- [ ] No string literals for network names outside the `Network` enum + +## Unit Tests Required + +### Network Enum Tests + +- `it_should_return_correct_network_name_for_database` +- `it_should_return_correct_network_name_for_metrics` +- `it_should_return_correct_network_name_for_visualization` +- `it_should_return_correct_network_name_for_proxy` +- `it_should_return_bridge_driver_for_all_networks` +- `it_should_serialize_network_to_name_string` +- `it_should_display_network_as_name` + +### Service Network Assignment Tests + +**Tracker:** + +- `it_should_connect_tracker_to_database_network_when_mysql_enabled` +- `it_should_connect_tracker_to_metrics_network_when_prometheus_enabled` +- `it_should_connect_tracker_to_proxy_network_when_tracker_needs_tls` +- `it_should_not_connect_tracker_to_database_network_when_mysql_disabled` +- `it_should_not_connect_tracker_to_metrics_network_when_prometheus_disabled` +- `it_should_not_connect_tracker_to_proxy_network_when_tracker_has_no_tls` + +**MySQL:** + +- `it_should_connect_mysql_to_database_network` + +**Prometheus:** + +- `it_should_connect_prometheus_to_metrics_network` +- `it_should_connect_prometheus_to_visualization_network_when_grafana_enabled` +- `it_should_not_connect_prometheus_to_visualization_network_when_grafana_disabled` + +**Grafana:** + +- `it_should_connect_grafana_to_visualization_network` +- `it_should_connect_grafana_to_proxy_network_when_grafana_has_tls` +- `it_should_not_connect_grafana_to_proxy_network_when_grafana_has_no_tls` + +**Caddy:** + +- `it_should_connect_caddy_to_all_networks_of_proxied_services` + +## Related Documentation + +- [Refactoring Plan - Phase 1](../refactors/plans/docker-compose-topology-domain-model.md#phase-1-domain-network-types-core-infrastructure) +- [Codebase Architecture](../../codebase-architecture.md) +- [DDD Layer Placement Guide](../../contributing/ddd-layer-placement.md) +- [Unit Testing Conventions](../../contributing/testing/unit-testing.md) + +## Notes + +- This phase can technically start in parallel with Phase 0, but depends on Phase 0 for the bind mount patterns and domain type conventions +- The `Network` enum is designed to be extended in Phase 2 for automatic network derivation +- Template output must remain identical to verify no regression diff --git a/docs/refactors/plans/docker-compose-topology-domain-model.md b/docs/refactors/plans/docker-compose-topology-domain-model.md index 4b7855aa..c1dc344c 100644 --- a/docs/refactors/plans/docker-compose-topology-domain-model.md +++ b/docs/refactors/plans/docker-compose-topology-domain-model.md @@ -32,16 +32,16 @@ This refactoring plan addresses architectural issues in the docker-compose templ **Bugs to Fix**: 1 **Total Postponed**: 0 **Total Discarded**: 0 -**Completed**: 0 +**Completed**: 6 **In Progress**: 0 -**Not Started**: 8 (including pre-phase task and bugs) +**Not Started**: 2 ### Phase Summary -- **Bugs to Fix (Priority)**: ⏳ 0/1 fixed (0%) - Fix before or during relevant phase -- **Pre-Phase - ADR Documentation**: ⏳ 0/1 completed (0%) -- **Phase 0 - Bind Mount Standardization (Medium Impact, Low Effort)**: ⏳ 0/2 completed (0%) -- **Phase 1 - Domain Network Types (High Impact, Medium Effort)**: ⏳ 0/2 completed (0%) +- **Bugs to Fix (Priority)**: ✅ 1/1 fixed (100%) - Fix before or during relevant phase +- **Pre-Phase - ADR Documentation**: ✅ 1/1 completed (100%) +- **Phase 0 - Bind Mount Standardization (Medium Impact, Low Effort)**: ✅ 2/2 completed (100%) +- **Phase 1 - Domain Network Types (High Impact, Medium Effort)**: ✅ 2/2 completed (100%) - **Phase 2 - Topology Aggregate & Network Derivation (High Impact, Medium Effort)**: ⏳ 0/2 completed (0%) ### Master Task List @@ -50,12 +50,12 @@ This table provides a single view of all work items for easy tracking: | ID | Phase | Task | Status | Depends On | Notes | | ------ | --------- | ----------------------------------------------------------- | -------------- | ---------- | ----------------------------------------------------------------------------------------- | -| BUG-01 | Priority | Remove invalid "Grafana without Prometheus" template branch | ⏳ Not Started | - | Dead code, see [BUG-01](#bug-01-template-handles-invalid-grafana-without-prometheus-case) | -| ADR-01 | Pre-Phase | Create ADR for Bind Mount Standardization | ⏳ Not Started | - | See [Pre-Phase Task](#pre-phase-task-create-adr-for-bind-mount-standardization) | -| P0.1 | Phase 0 | Convert Named Volumes to Bind Mounts | ⏳ Not Started | ADR-01 | See [Proposal 0.1](#proposal-01-convert-named-volumes-to-bind-mounts) | -| P0.2 | Phase 0 | Create BindMount Domain Type | ⏳ Not Started | P0.1 | See [Proposal 0.2](#proposal-02-create-bindmount-domain-type) | -| P1.1 | Phase 1 | Create Network Domain Types | ⏳ Not Started | - | See [Proposal 1.1](#proposal-11-create-network-domain-types) | -| P1.2 | Phase 1 | Migrate Service Configs to Use Network Enum | ⏳ Not Started | P1.1 | See [Proposal 1.2](#proposal-12-migrate-service-configs-to-use-network-enum) | +| BUG-01 | Priority | Remove invalid "Grafana without Prometheus" template branch | ✅ Completed | - | Dead code, see [BUG-01](#bug-01-template-handles-invalid-grafana-without-prometheus-case) | +| ADR-01 | Pre-Phase | Create ADR for Bind Mount Standardization | ✅ Completed | - | See [Pre-Phase Task](#pre-phase-task-create-adr-for-bind-mount-standardization) | +| P0.1 | Phase 0 | Convert Named Volumes to Bind Mounts | ✅ Completed | ADR-01 | See [Proposal 0.1](#proposal-01-convert-named-volumes-to-bind-mounts) | +| P0.2 | Phase 0 | Create BindMount Domain Type | ✅ Completed | P0.1 | See [Proposal 0.2](#proposal-02-create-bindmount-domain-type) | +| P1.1 | Phase 1 | Create Network Domain Types | ✅ Completed | - | See [Proposal 1.1](#proposal-11-create-network-domain-types) | +| P1.2 | Phase 1 | Migrate Service Configs to Use Network Enum | ✅ Completed | P1.1 | See [Proposal 1.2](#proposal-12-migrate-service-configs-to-use-network-enum) | | P2.1 | Phase 2 | Create DockerComposeTopology Aggregate | ⏳ Not Started | P1.2 | See [Proposal 2.1](#proposal-21-create-dockercomposetopology-aggregate) | | P2.2 | Phase 2 | Derive Required Networks in Context | ⏳ Not Started | P2.1 | See [Proposal 2.2](#proposal-22-derive-required-networks-in-context) | @@ -72,9 +72,14 @@ This table provides a single view of all work items for easy tracking: Track completed items with dates and any relevant notes: -| Date | ID | Task | Commit/PR | Notes | -| ---- | --- | ------------------------ | --------- | ----- | -| - | - | _No items completed yet_ | - | - | +| Date | ID | Task | Commit/PR | Notes | +| ---------- | ------ | ----------------------------------------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------- | +| 2025-07-17 | ADR-01 | Create ADR for Bind Mount Standardization | [PR #289](https://github.com/torrust/torrust-tracker-deployer/pull/289) | Documented 9 reasons for bind mount standardization | +| 2025-07-17 | BUG-01 | Remove invalid "Grafana without Prometheus" template branch | [PR #291](https://github.com/torrust/torrust-tracker-deployer/pull/291) | Removed dead code from docker-compose template | +| 2025-07-17 | P0.1 | Convert Named Volumes to Bind Mounts | [PR #293](https://github.com/torrust/torrust-tracker-deployer/pull/293) | Bind mounts with BindMount domain type | +| 2025-07-17 | P0.2 | Create BindMount Domain Type | [PR #293](https://github.com/torrust/torrust-tracker-deployer/pull/293) | Storage playbooks with proper ownership | +| 2025-07-17 | P1.1 | Create Network Domain Types | [PR #295](https://github.com/torrust/torrust-tracker-deployer/pull/295) | Network enum in src/domain/topology/ | +| 2025-07-17 | P1.2 | Migrate Service Configs to Use Network Enum | [PR #295](https://github.com/torrust/torrust-tracker-deployer/pull/295) | All service configs use Vec | ### Discarded Proposals diff --git a/src/domain/mod.rs b/src/domain/mod.rs index ce2541da..3eadb53e 100644 --- a/src/domain/mod.rs +++ b/src/domain/mod.rs @@ -12,6 +12,7 @@ //! - `profile_name` - LXD profile name validation and management //! - `provider` - Infrastructure provider types (LXD, Hetzner) and configuration //! - `template` - Core template domain models and business logic +//! - `topology` - Docker Compose topology domain types (networks, services) pub mod environment; pub mod grafana; @@ -21,6 +22,7 @@ pub mod profile_name; pub mod prometheus; pub mod provider; pub mod template; +pub mod topology; pub mod tracker; // Re-export commonly used domain types for convenience @@ -33,3 +35,4 @@ pub use instance_name::{InstanceName, InstanceNameError}; pub use profile_name::{ProfileName, ProfileNameError}; pub use provider::{HetznerConfig, LxdConfig, Provider, ProviderConfig}; pub use template::{TemplateEngine, TemplateEngineError, TemplateManager, TemplateManagerError}; +pub use topology::Network; diff --git a/src/domain/topology/mod.rs b/src/domain/topology/mod.rs new file mode 100644 index 00000000..5ac30156 --- /dev/null +++ b/src/domain/topology/mod.rs @@ -0,0 +1,20 @@ +//! Docker Compose Topology Domain Types +//! +//! This module contains domain types for Docker Compose topology elements, +//! including networks, services, and their relationships. +//! +//! ## Design Principles +//! +//! These types represent business concepts with strong typing: +//! - Type-safe network references (no string typos) +//! - Single source of truth for network names +//! - Domain-level invariant enforcement +//! +//! ## Components +//! +//! - [`Network`] - Docker Compose network enum representing isolation boundaries + +pub mod network; + +// Re-export main types for convenience +pub use network::Network; diff --git a/src/domain/topology/network.rs b/src/domain/topology/network.rs new file mode 100644 index 00000000..b0a604e3 --- /dev/null +++ b/src/domain/topology/network.rs @@ -0,0 +1,313 @@ +//! Docker Compose Network Domain Type +//! +//! This module defines the [`Network`] enum representing Docker Compose networks +//! used for service isolation. Each network serves a specific security purpose +//! in the deployment topology. +//! +//! ## Network Purposes +//! +//! | Network | Purpose | Connected Services | +//! |---------|---------|-------------------| +//! | `Database` | Isolates database access | Tracker ↔ `MySQL` | +//! | `Metrics` | Metrics scraping | Tracker ↔ Prometheus | +//! | `Visualization` | Dashboard queries | Prometheus ↔ Grafana | +//! | `Proxy` | TLS termination | Caddy ↔ backend services | +//! +//! ## Usage +//! +//! ```rust +//! use torrust_tracker_deployer_lib::domain::topology::Network; +//! +//! let network = Network::Metrics; +//! assert_eq!(network.name(), "metrics_network"); +//! assert_eq!(network.driver(), "bridge"); +//! ``` + +use std::fmt; + +use serde::Serialize; + +/// Docker Compose networks used for service isolation +/// +/// Each network serves a specific security purpose: +/// - `Database`: Isolates database access to only the tracker +/// - `Metrics`: Allows Prometheus to scrape tracker metrics +/// - `Visualization`: Allows Grafana to query Prometheus +/// - `Proxy`: Allows Caddy to reverse proxy to backend services +/// +/// # Serialization +/// +/// When serialized, the network outputs its name string (e.g., `"metrics_network"`), +/// making it directly usable in Tera templates: +/// +/// ```yaml +/// networks: +/// - {{ network }} # Renders as "metrics_network" +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Network { + /// Network for database access (Tracker ↔ `MySQL`) + /// + /// Only the tracker and `MySQL` are connected to this network, + /// ensuring database isolation from other services. + Database, + + /// Network for metrics scraping (Tracker ↔ Prometheus) + /// + /// Allows Prometheus to scrape metrics from the tracker + /// while keeping Prometheus isolated from other traffic. + Metrics, + + /// Network for visualization queries (Prometheus ↔ Grafana) + /// + /// Allows Grafana to query Prometheus for dashboard data. + Visualization, + + /// Network for TLS proxy (Caddy ↔ backend services) + /// + /// Allows Caddy to reverse proxy to services that need + /// TLS termination (tracker API, Grafana, etc.). + Proxy, +} + +impl Network { + /// Returns the network name as used in docker-compose.yml + /// + /// # Examples + /// + /// ```rust + /// use torrust_tracker_deployer_lib::domain::topology::Network; + /// + /// assert_eq!(Network::Database.name(), "database_network"); + /// assert_eq!(Network::Metrics.name(), "metrics_network"); + /// assert_eq!(Network::Visualization.name(), "visualization_network"); + /// assert_eq!(Network::Proxy.name(), "proxy_network"); + /// ``` + #[must_use] + pub fn name(&self) -> &'static str { + match self { + Network::Database => "database_network", + Network::Metrics => "metrics_network", + Network::Visualization => "visualization_network", + Network::Proxy => "proxy_network", + } + } + + /// Returns the network driver + /// + /// Currently all networks use the `bridge` driver. + /// + /// # Examples + /// + /// ```rust + /// use torrust_tracker_deployer_lib::domain::topology::Network; + /// + /// assert_eq!(Network::Database.driver(), "bridge"); + /// ``` + #[must_use] + pub fn driver(&self) -> &'static str { + "bridge" + } + + /// Returns all network variants + /// + /// Useful for iteration when generating the global networks section + /// in docker-compose.yml. + /// + /// # Examples + /// + /// ```rust + /// use torrust_tracker_deployer_lib::domain::topology::Network; + /// + /// let all = Network::all(); + /// assert_eq!(all.len(), 4); + /// ``` + #[must_use] + pub fn all() -> &'static [Network] { + &[ + Network::Database, + Network::Metrics, + Network::Visualization, + Network::Proxy, + ] + } +} + +impl fmt::Display for Network { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name()) + } +} + +/// Custom serialization to output the network name string +/// +/// This allows the Network enum to be used directly in Tera templates +/// without needing wrapper types or custom serialization logic. +impl Serialize for Network { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.name()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // ========================================================================== + // Network name tests + // ========================================================================== + + #[test] + fn it_should_return_correct_network_name_for_database() { + assert_eq!(Network::Database.name(), "database_network"); + } + + #[test] + fn it_should_return_correct_network_name_for_metrics() { + assert_eq!(Network::Metrics.name(), "metrics_network"); + } + + #[test] + fn it_should_return_correct_network_name_for_visualization() { + assert_eq!(Network::Visualization.name(), "visualization_network"); + } + + #[test] + fn it_should_return_correct_network_name_for_proxy() { + assert_eq!(Network::Proxy.name(), "proxy_network"); + } + + // ========================================================================== + // Driver tests + // ========================================================================== + + #[test] + fn it_should_return_bridge_driver_for_database_network() { + assert_eq!(Network::Database.driver(), "bridge"); + } + + #[test] + fn it_should_return_bridge_driver_for_metrics_network() { + assert_eq!(Network::Metrics.driver(), "bridge"); + } + + #[test] + fn it_should_return_bridge_driver_for_visualization_network() { + assert_eq!(Network::Visualization.driver(), "bridge"); + } + + #[test] + fn it_should_return_bridge_driver_for_proxy_network() { + assert_eq!(Network::Proxy.driver(), "bridge"); + } + + // ========================================================================== + // Display tests + // ========================================================================== + + #[test] + fn it_should_display_network_as_name() { + assert_eq!(format!("{}", Network::Database), "database_network"); + assert_eq!(format!("{}", Network::Metrics), "metrics_network"); + assert_eq!( + format!("{}", Network::Visualization), + "visualization_network" + ); + assert_eq!(format!("{}", Network::Proxy), "proxy_network"); + } + + // ========================================================================== + // Serialization tests + // ========================================================================== + + #[test] + fn it_should_serialize_network_to_name_string() { + let json = serde_json::to_string(&Network::Metrics).unwrap(); + assert_eq!(json, "\"metrics_network\""); + } + + #[test] + fn it_should_serialize_all_networks_correctly() { + assert_eq!( + serde_json::to_string(&Network::Database).unwrap(), + "\"database_network\"" + ); + assert_eq!( + serde_json::to_string(&Network::Metrics).unwrap(), + "\"metrics_network\"" + ); + assert_eq!( + serde_json::to_string(&Network::Visualization).unwrap(), + "\"visualization_network\"" + ); + assert_eq!( + serde_json::to_string(&Network::Proxy).unwrap(), + "\"proxy_network\"" + ); + } + + // ========================================================================== + // All networks tests + // ========================================================================== + + #[test] + fn it_should_return_all_four_networks() { + let all = Network::all(); + + assert_eq!(all.len(), 4); + assert!(all.contains(&Network::Database)); + assert!(all.contains(&Network::Metrics)); + assert!(all.contains(&Network::Visualization)); + assert!(all.contains(&Network::Proxy)); + } + + // ========================================================================== + // Equality tests + // ========================================================================== + + #[test] + fn it_should_compare_networks_for_equality() { + assert_eq!(Network::Database, Network::Database); + assert_ne!(Network::Database, Network::Metrics); + } + + // ========================================================================== + // Clone and Copy tests + // ========================================================================== + + #[test] + fn it_should_be_copyable() { + let network = Network::Metrics; + let copied = network; // Copy + assert_eq!(network, copied); + } + + #[test] + fn it_should_be_clonable() { + let network = Network::Metrics; + #[allow(clippy::clone_on_copy)] + let cloned = network.clone(); + assert_eq!(network, cloned); + } + + // ========================================================================== + // Hash tests (for use in HashSet/HashMap) + // ========================================================================== + + #[test] + fn it_should_be_hashable() { + use std::collections::HashSet; + + let mut set = HashSet::new(); + set.insert(Network::Database); + set.insert(Network::Metrics); + set.insert(Network::Database); // Duplicate + + assert_eq!(set.len(), 2); + assert!(set.contains(&Network::Database)); + assert!(set.contains(&Network::Metrics)); + } +} diff --git a/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/caddy.rs b/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/caddy.rs index fab4d260..652af4af 100644 --- a/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/caddy.rs +++ b/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/caddy.rs @@ -16,8 +16,7 @@ use serde::Serialize; -/// Network names used by the Caddy service -const PROXY_NETWORK: &str = "proxy_network"; +use crate::domain::topology::Network; /// Caddy reverse proxy service configuration for Docker Compose /// @@ -29,9 +28,10 @@ const PROXY_NETWORK: &str = "proxy_network"; /// /// ```rust /// use torrust_tracker_deployer_lib::infrastructure::templating::docker_compose::template::wrappers::docker_compose::context::CaddyServiceConfig; +/// use torrust_tracker_deployer_lib::domain::topology::Network; /// /// let caddy = CaddyServiceConfig::new(); -/// assert_eq!(caddy.networks, vec!["proxy_network"]); +/// assert_eq!(caddy.networks, vec![Network::Proxy]); /// ``` #[derive(Debug, Clone, Serialize, PartialEq)] pub struct CaddyServiceConfig { @@ -39,7 +39,7 @@ pub struct CaddyServiceConfig { /// /// Caddy always connects to `proxy_network` for reverse proxying /// to backend services (tracker API, HTTP trackers, Grafana). - pub networks: Vec, + pub networks: Vec, } impl CaddyServiceConfig { @@ -50,7 +50,7 @@ impl CaddyServiceConfig { #[must_use] pub fn new() -> Self { Self { - networks: vec![PROXY_NETWORK.to_string()], + networks: vec![Network::Proxy], } } } @@ -66,25 +66,26 @@ mod tests { use super::*; #[test] - fn it_should_create_caddy_config_with_proxy_network() { + fn it_should_connect_caddy_to_proxy_network() { let caddy = CaddyServiceConfig::new(); - assert_eq!(caddy.networks, vec!["proxy_network"]); + assert_eq!(caddy.networks, vec![Network::Proxy]); } #[test] fn it_should_implement_default() { let caddy = CaddyServiceConfig::default(); - assert_eq!(caddy.networks, vec!["proxy_network"]); + assert_eq!(caddy.networks, vec![Network::Proxy]); } #[test] - fn it_should_serialize_to_json() { + fn it_should_serialize_network_to_name_string() { let caddy = CaddyServiceConfig::new(); let json = serde_json::to_value(&caddy).expect("serialization should succeed"); + // Network serializes to its name string for template compatibility assert_eq!(json["networks"][0], "proxy_network"); } } diff --git a/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/grafana.rs b/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/grafana.rs index f3a1e0aa..1a506993 100644 --- a/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/grafana.rs +++ b/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/grafana.rs @@ -3,6 +3,7 @@ // External crates use serde::Serialize; +use crate::domain::topology::Network; use crate::shared::secrets::Password; /// Grafana service configuration for Docker Compose @@ -24,7 +25,7 @@ pub struct GrafanaServiceConfig { /// Pre-computed list based on enabled features: /// - Always includes `visualization_network` (queries Prometheus) /// - Includes `proxy_network` if Caddy TLS proxy is enabled - pub networks: Vec, + pub networks: Vec, } impl GrafanaServiceConfig { @@ -54,11 +55,11 @@ impl GrafanaServiceConfig { } /// Computes the list of networks for the Grafana service - fn compute_networks(has_caddy: bool) -> Vec { - let mut networks = vec!["visualization_network".to_string()]; + fn compute_networks(has_caddy: bool) -> Vec { + let mut networks = vec![Network::Visualization]; if has_caddy { - networks.push("proxy_network".to_string()); + networks.push(Network::Proxy); } networks @@ -70,25 +71,42 @@ mod tests { use super::*; #[test] - fn it_should_create_grafana_config_with_only_visualization_network_when_caddy_disabled() { + fn it_should_connect_grafana_to_visualization_network() { let config = GrafanaServiceConfig::new("admin".to_string(), Password::new("password"), false, false); - assert_eq!(config.admin_user, "admin"); - assert!(!config.has_tls); - assert_eq!(config.networks, vec!["visualization_network"]); + assert!(config.networks.contains(&Network::Visualization)); } #[test] - fn it_should_create_grafana_config_with_both_networks_when_caddy_enabled() { + fn it_should_not_connect_grafana_to_proxy_network_when_caddy_disabled() { + let config = + GrafanaServiceConfig::new("admin".to_string(), Password::new("password"), false, false); + + assert_eq!(config.networks, vec![Network::Visualization]); + assert!(!config.networks.contains(&Network::Proxy)); + } + + #[test] + fn it_should_connect_grafana_to_proxy_network_when_caddy_enabled() { let config = GrafanaServiceConfig::new("admin".to_string(), Password::new("password"), true, true); - assert_eq!(config.admin_user, "admin"); - assert!(config.has_tls); assert_eq!( config.networks, - vec!["visualization_network", "proxy_network"] + vec![Network::Visualization, Network::Proxy] ); } + + #[test] + fn it_should_serialize_networks_to_name_strings() { + let config = + GrafanaServiceConfig::new("admin".to_string(), Password::new("password"), true, true); + + let json = serde_json::to_value(&config).expect("serialization should succeed"); + + // Networks serialize to their name strings for template compatibility + assert_eq!(json["networks"][0], "visualization_network"); + assert_eq!(json["networks"][1], "proxy_network"); + } } diff --git a/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/mod.rs b/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/mod.rs index 9309c38e..ddd6ed50 100644 --- a/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/mod.rs +++ b/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/mod.rs @@ -297,6 +297,8 @@ mod tests { #[test] fn it_should_compute_prometheus_networks_without_grafana() { + use crate::domain::topology::Network; + let tracker = test_tracker_config(); let prometheus_config = PrometheusConfig::new(std::num::NonZeroU32::new(15).expect("15 is non-zero")); @@ -305,12 +307,13 @@ mod tests { .build(); let prometheus = context.prometheus().unwrap(); - assert_eq!(prometheus.networks, vec!["metrics_network"]); + assert_eq!(prometheus.networks, vec![Network::Metrics]); } #[test] fn it_should_compute_prometheus_networks_with_grafana() { use crate::domain::grafana::GrafanaConfig; + use crate::domain::topology::Network; let tracker = test_tracker_config(); let prometheus_config = @@ -325,13 +328,14 @@ mod tests { let prometheus = context.prometheus().unwrap(); assert_eq!( prometheus.networks, - vec!["metrics_network", "visualization_network"] + vec![Network::Metrics, Network::Visualization] ); } #[test] fn it_should_compute_grafana_networks_without_caddy() { use crate::domain::grafana::GrafanaConfig; + use crate::domain::topology::Network; let tracker = test_tracker_config(); let grafana_config = @@ -341,13 +345,14 @@ mod tests { .build(); let grafana = context.grafana().unwrap(); - assert_eq!(grafana.networks, vec!["visualization_network"]); + assert_eq!(grafana.networks, vec![Network::Visualization]); assert!(!grafana.has_tls); } #[test] fn it_should_compute_grafana_networks_with_caddy() { use crate::domain::grafana::GrafanaConfig; + use crate::domain::topology::Network; let tracker = test_tracker_config(); let grafana_config = @@ -360,7 +365,7 @@ mod tests { let grafana = context.grafana().unwrap(); assert_eq!( grafana.networks, - vec!["visualization_network", "proxy_network"] + vec![Network::Visualization, Network::Proxy] ); } } diff --git a/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/mysql.rs b/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/mysql.rs index 3ff842f1..fc00a29b 100644 --- a/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/mysql.rs +++ b/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/mysql.rs @@ -17,8 +17,7 @@ use serde::Serialize; -/// Network names used by the `MySQL` service -const DATABASE_NETWORK: &str = "database_network"; +use crate::domain::topology::Network; /// `MySQL` service configuration for Docker Compose /// @@ -32,7 +31,7 @@ const DATABASE_NETWORK: &str = "database_network"; /// use torrust_tracker_deployer_lib::infrastructure::templating::docker_compose::template::wrappers::docker_compose::context::MysqlServiceConfig; /// /// let mysql = MysqlServiceConfig::new(); -/// assert_eq!(mysql.networks, vec!["database_network"]); +/// assert_eq!(mysql.networks, vec![torrust_tracker_deployer_lib::domain::topology::Network::Database]); /// ``` #[derive(Debug, Clone, Serialize, PartialEq)] pub struct MysqlServiceConfig { @@ -40,7 +39,7 @@ pub struct MysqlServiceConfig { /// /// `MySQL` only connects to `database_network` for isolation. /// Only the tracker can access `MySQL` through this network. - pub networks: Vec, + pub networks: Vec, } impl MysqlServiceConfig { @@ -51,7 +50,7 @@ impl MysqlServiceConfig { #[must_use] pub fn new() -> Self { Self { - networks: vec![DATABASE_NETWORK.to_string()], + networks: vec![Network::Database], } } } @@ -67,25 +66,26 @@ mod tests { use super::*; #[test] - fn it_should_create_mysql_config_with_database_network() { + fn it_should_connect_mysql_to_database_network() { let mysql = MysqlServiceConfig::new(); - assert_eq!(mysql.networks, vec!["database_network"]); + assert_eq!(mysql.networks, vec![Network::Database]); } #[test] fn it_should_implement_default() { let mysql = MysqlServiceConfig::default(); - assert_eq!(mysql.networks, vec!["database_network"]); + assert_eq!(mysql.networks, vec![Network::Database]); } #[test] - fn it_should_serialize_to_json() { + fn it_should_serialize_network_to_name_string() { let mysql = MysqlServiceConfig::new(); let json = serde_json::to_value(&mysql).expect("serialization should succeed"); + // Network serializes to its name string for template compatibility assert_eq!(json["networks"][0], "database_network"); } } diff --git a/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/prometheus.rs b/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/prometheus.rs index 8edf811b..7b8be783 100644 --- a/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/prometheus.rs +++ b/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/prometheus.rs @@ -3,6 +3,8 @@ // External crates use serde::Serialize; +use crate::domain::topology::Network; + /// Prometheus service configuration for Docker Compose /// /// Contains all configuration needed for the Prometheus service in Docker Compose, @@ -17,7 +19,7 @@ pub struct PrometheusServiceConfig { /// Pre-computed list based on enabled features: /// - Always includes `metrics_network` (scrapes metrics from tracker) /// - Includes `visualization_network` if Grafana is enabled - pub networks: Vec, + pub networks: Vec, } impl PrometheusServiceConfig { @@ -38,11 +40,11 @@ impl PrometheusServiceConfig { } /// Computes the list of networks for the Prometheus service - fn compute_networks(has_grafana: bool) -> Vec { - let mut networks = vec!["metrics_network".to_string()]; + fn compute_networks(has_grafana: bool) -> Vec { + let mut networks = vec![Network::Metrics]; if has_grafana { - networks.push("visualization_network".to_string()); + networks.push(Network::Visualization); } networks @@ -54,21 +56,38 @@ mod tests { use super::*; #[test] - fn it_should_create_prometheus_config_with_only_metrics_network_when_grafana_disabled() { + fn it_should_connect_prometheus_to_metrics_network() { + let config = PrometheusServiceConfig::new(15, false); + + assert!(config.networks.contains(&Network::Metrics)); + } + + #[test] + fn it_should_not_connect_prometheus_to_visualization_network_when_grafana_disabled() { let config = PrometheusServiceConfig::new(15, false); - assert_eq!(config.scrape_interval_in_secs, 15); - assert_eq!(config.networks, vec!["metrics_network"]); + assert_eq!(config.networks, vec![Network::Metrics]); + assert!(!config.networks.contains(&Network::Visualization)); } #[test] - fn it_should_create_prometheus_config_with_both_networks_when_grafana_enabled() { + fn it_should_connect_prometheus_to_visualization_network_when_grafana_enabled() { let config = PrometheusServiceConfig::new(30, true); - assert_eq!(config.scrape_interval_in_secs, 30); assert_eq!( config.networks, - vec!["metrics_network", "visualization_network"] + vec![Network::Metrics, Network::Visualization] ); } + + #[test] + fn it_should_serialize_networks_to_name_strings() { + let config = PrometheusServiceConfig::new(15, true); + + let json = serde_json::to_value(&config).expect("serialization should succeed"); + + // Networks serialize to their name strings for template compatibility + assert_eq!(json["networks"][0], "metrics_network"); + assert_eq!(json["networks"][1], "visualization_network"); + } } diff --git a/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/tracker.rs b/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/tracker.rs index fc5e81ba..7ce5f8d5 100644 --- a/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/tracker.rs +++ b/src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/tracker.rs @@ -3,6 +3,8 @@ // External crates use serde::Serialize; +use crate::domain::topology::Network; + /// Tracker service configuration for Docker Compose /// /// Contains all configuration needed for the tracker service in Docker Compose, @@ -30,7 +32,7 @@ pub struct TrackerServiceConfig { /// Networks the tracker service should connect to /// /// Pre-computed list based on enabled features (prometheus, mysql, caddy). - pub networks: Vec, + pub networks: Vec, } impl TrackerServiceConfig { @@ -73,17 +75,17 @@ impl TrackerServiceConfig { } /// Computes the list of networks for the tracker service - fn compute_networks(has_prometheus: bool, has_mysql: bool, has_caddy: bool) -> Vec { + fn compute_networks(has_prometheus: bool, has_mysql: bool, has_caddy: bool) -> Vec { let mut networks = Vec::new(); if has_prometheus { - networks.push("metrics_network".to_string()); + networks.push(Network::Metrics); } if has_mysql { - networks.push("database_network".to_string()); + networks.push(Network::Database); } if has_caddy { - networks.push("proxy_network".to_string()); + networks.push(Network::Proxy); } networks @@ -92,3 +94,158 @@ impl TrackerServiceConfig { // Type alias for backward compatibility pub type TrackerPorts = TrackerServiceConfig; + +#[cfg(test)] +mod tests { + use super::*; + + // ========================================================================== + // Network assignment tests + // ========================================================================== + + #[test] + fn it_should_connect_tracker_to_metrics_network_when_prometheus_enabled() { + let config = TrackerServiceConfig::new( + vec![6969], + vec![], + 1212, + false, + true, // has_prometheus + false, // has_mysql + false, // has_caddy + ); + + assert!(config.networks.contains(&Network::Metrics)); + } + + #[test] + fn it_should_not_connect_tracker_to_metrics_network_when_prometheus_disabled() { + let config = TrackerServiceConfig::new( + vec![6969], + vec![], + 1212, + false, + false, // has_prometheus + false, // has_mysql + false, // has_caddy + ); + + assert!(!config.networks.contains(&Network::Metrics)); + } + + #[test] + fn it_should_connect_tracker_to_database_network_when_mysql_enabled() { + let config = TrackerServiceConfig::new( + vec![6969], + vec![], + 1212, + false, + false, // has_prometheus + true, // has_mysql + false, // has_caddy + ); + + assert!(config.networks.contains(&Network::Database)); + } + + #[test] + fn it_should_not_connect_tracker_to_database_network_when_mysql_disabled() { + let config = TrackerServiceConfig::new( + vec![6969], + vec![], + 1212, + false, + false, // has_prometheus + false, // has_mysql (SQLite) + false, // has_caddy + ); + + assert!(!config.networks.contains(&Network::Database)); + } + + #[test] + fn it_should_connect_tracker_to_proxy_network_when_caddy_enabled() { + let config = TrackerServiceConfig::new( + vec![6969], + vec![], + 1212, + true, // http_api_has_tls + false, // has_prometheus + false, // has_mysql + true, // has_caddy + ); + + assert!(config.networks.contains(&Network::Proxy)); + } + + #[test] + fn it_should_not_connect_tracker_to_proxy_network_when_caddy_disabled() { + let config = TrackerServiceConfig::new( + vec![6969], + vec![], + 1212, + false, + false, // has_prometheus + false, // has_mysql + false, // has_caddy + ); + + assert!(!config.networks.contains(&Network::Proxy)); + } + + #[test] + fn it_should_connect_tracker_to_all_networks_when_all_services_enabled() { + let config = TrackerServiceConfig::new( + vec![6969], + vec![], + 1212, + true, + true, // has_prometheus + true, // has_mysql + true, // has_caddy + ); + + assert_eq!( + config.networks, + vec![Network::Metrics, Network::Database, Network::Proxy] + ); + } + + #[test] + fn it_should_have_no_networks_when_minimal_deployment() { + let config = TrackerServiceConfig::new( + vec![6969], + vec![], + 1212, + false, + false, // has_prometheus + false, // has_mysql (SQLite) + false, // has_caddy + ); + + assert!(config.networks.is_empty()); + } + + // ========================================================================== + // Serialization tests + // ========================================================================== + + #[test] + fn it_should_serialize_networks_to_name_strings() { + let config = TrackerServiceConfig::new( + vec![6969], + vec![], + 1212, + true, + true, // has_prometheus + true, // has_mysql + false, // has_caddy + ); + + let json = serde_json::to_value(&config).expect("serialization should succeed"); + + // Networks serialize to their name strings for template compatibility + assert_eq!(json["networks"][0], "metrics_network"); + assert_eq!(json["networks"][1], "database_network"); + } +}