Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ priority: p0
github-issue: 1979
spec-path: docs/issues/open/1979-1978-copy-configuration-schema-v2-to-v3-baseline.md
branch: "config-copy-v2-to-v3-baseline"
related-pr: null
related-pr: 1999
last-updated-utc: 2026-07-13 21:00
semantic-links:
skill-links:
Expand Down Expand Up @@ -59,17 +59,17 @@ This approach:

## Implementation Plan

| ID | Status | Task | Notes |
| --- | ------ | ------------------------------------------------------------ | ----------------------------------------------------------------------------- |
| T1 | TODO | Copy `v2_0_0/` directory to `v3_0_0/` | `cp -r packages/configuration/src/v2_0_0/ packages/configuration/src/v3_0_0/` |
| T2 | TODO | Update `v3_0_0/mod.rs` to use `crate::v3_0_0` internal paths | Fix module references within the copied files |
| T3 | TODO | Copy `logging.rs` into `v2_0_0/logging.rs` | Crate-root `logging.rs` (TraceStyle, setup, tracing_init) → v2_0_0 |
| T4 | TODO | Copy `logging.rs` into `v3_0_0/logging.rs` | Same content as T3; v3 gets its own copy |
| T5 | TODO | Update `lib.rs` to expose `pub mod v3_0_0` | Alongside existing `pub mod v2_0_0` |
| T6 | TODO | Update default config files to `schema_version = "3.0.0"` | In `share/default/config/` |
| T7 | TODO | Wire application entry point to use `v3_0_0` by default | Update `lib.rs` or `container.rs` default schema selection |
| T8 | TODO | Add smoke tests: deserialize default v3 config | Verify v3_0_0 can parse the default config |
| T9 | TODO | Run `linter all` and full test suite | |
| ID | Status | Task | Notes |
| --- | ---------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| T1 | DONE | Copy `v2_0_0/` directory to `v3_0_0/` | `cp -r packages/configuration/src/v2_0_0/ packages/configuration/src/v3_0_0/` |
| T2 | DONE | Update `v3_0_0/mod.rs` to use `crate::v3_0_0` internal paths | Fixed all doc links, VERSION constant, test imports, and schema_version strings |
| T3 | DONE | Copy `logging.rs` into `v2_0_0/logging.rs` | Merged TraceStyle/setup/tracing_init into the versioned logging.rs; added module-level doc comment |
| T4 | DONE | Copy `logging.rs` into `v3_0_0/logging.rs` | Same content as T3; v3 gets its own copy |
| T5 | DONE | Update `lib.rs` to expose `pub mod v3_0_0` | Added alongside existing `pub mod v2_0_0`; added `Metadata::with_schema_version` helper; global re-exports stay at v2 |
| T6 | DEFERRED → #1980 | Update default config files to `schema_version = "3.0.0"` | Cannot be done while bootstrap still uses `v2_0_0::Configuration`; config files and bootstrap switch together in #1980 |
| T7 | DEFERRED → #1980 | Wire application entry point to use `v3_0_0` by default | Requires updating bootstrap + all consumers; this is exactly the scope of subissue #1980 |
| T8 | DONE | Add smoke tests: deserialize default v3 config | Added `smoke::v3_configuration_should_load_when_schema_version_is_3_0_0` and `smoke::v3_configuration_should_reject_schema_version_2_0_0` |
| T9 | DONE | Run `linter all` and full test suite | All 48 test suites pass (0 failures) |

## Progress Tracking

Expand All @@ -88,16 +88,17 @@ This approach:

- 2026-07-13 21:00 UTC - josecelano - Initial spec drafted
- 2026-07-15 00:00 UTC - josecelano - GitHub issue #1979 created; spec moved to `docs/issues/open/1979-1978-copy-configuration-schema-v2-to-v3-baseline.md`
- 2026-07-20 00:00 UTC - agent - Implementation completed: T1–T5 and T8–T9 done; T6/T7 deferred to #1980 (consumer migration must happen atomically)

## Acceptance Criteria

- [ ] AC1: `packages/configuration/src/v3_0_0/` exists as an exact copy of `v2_0_0/`
- [ ] AC2: `lib.rs` exposes both `v2_0_0` and `v3_0_0` modules
- [ ] AC3: Application uses `v3_0_0` by default
- [ ] AC4: All existing tests pass (v2 unchanged)
- [ ] AC5: Default config files reference `schema_version = "3.0.0"`
- [ ] `linter all` exits with code `0`
- [ ] Relevant tests pass
- [x] AC1: `packages/configuration/src/v3_0_0/` exists as an exact copy of `v2_0_0/`
- [x] AC2: `lib.rs` exposes both `v2_0_0` and `v3_0_0` modules
- [ ] AC3: Application uses `v3_0_0` by default — **DEFERRED to #1980** (requires switching bootstrap + all consumers atomically)
- [x] AC4: All existing tests pass (v2 unchanged)
- [ ] AC5: Default config files reference `schema_version = "3.0.0"` — **DEFERRED to #1980** (config files must match the active parser)
- [x] `linter all` exits with code `0`
- [x] Relevant tests pass (48 suites, 0 failures)

## Verification Plan

Expand All @@ -116,13 +117,13 @@ This approach:

### Acceptance Verification

| AC ID | Status | Evidence |
| ----- | ------ | -------- |
| AC1 | TODO | |
| AC2 | TODO | |
| AC3 | TODO | |
| AC4 | TODO | |
| AC5 | TODO | |
| AC ID | Status | Evidence |
| ----- | -------- | -------------------------------------------------------------------------------- |
| AC1 | DONE | `packages/configuration/src/v3_0_0/` exists with all 9 files mirroring `v2_0_0/` |
| AC2 | DONE | `lib.rs` has `pub mod v2_0_0` and `pub mod v3_0_0` |
| AC3 | DEFERRED | Deferred to #1980; requires switching bootstrap and all consumers atomically |
| AC4 | DONE | All 48 test suites pass; v2_0_0 tests unchanged |
| AC5 | DEFERRED | Deferred to #1980; config files must match the parser the bootstrap uses |

## Risks and Trade-offs

Expand Down
16 changes: 15 additions & 1 deletion packages/configuration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
//! This module contains the configuration data structures for the
//! Torrust Tracker, which is a `BitTorrent` tracker server.
//!
//! The current version for configuration is [`v2_0_0`].
//! The current schema version is `v3_0_0` (in progress).
//! The previous version [`v2_0_0`] is kept for backward compatibility.
//! Global re-exports still point to `v2_0_0` and will be migrated to `v3_0_0`
//! in the final cleanup subissue (#1980) once all v3 changes are complete.
pub mod logging;
pub mod v2_0_0;
pub mod v3_0_0;
pub mod validator;

use std::collections::HashMap;
Expand Down Expand Up @@ -71,6 +75,16 @@ impl Default for Metadata {
}

impl Metadata {
/// Creates a `Metadata` with a specific schema version, keeping other fields at their defaults.
#[must_use]
pub fn with_schema_version(schema_version: Version) -> Self {
Self {
app: Self::default_app(),
purpose: Self::default_purpose(),
schema_version,
}
}

fn default_app() -> App {
App::TorrustTracker
}
Expand Down
2 changes: 1 addition & 1 deletion packages/configuration/src/logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use std::sync::Once;

use tracing::level_filters::LevelFilter;

use crate::{Logging, Threshold};
use crate::v2_0_0::logging::{Logging, Threshold};

static INIT: Once = Once::new();

Expand Down
2 changes: 1 addition & 1 deletion packages/configuration/src/v2_0_0/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub struct Core {
#[serde(default = "Core::default_tracker_policy")]
pub tracker_policy: TrackerPolicy,

/// Weather the tracker should collect statistics about tracker usage.
/// Whether the tracker should collect statistics about tracker usage.
/// If enabled, the tracker will collect statistics like the number of
/// connections handled, the number of announce requests handled, etc.
/// Refer to the [`Tracker`](https://docs.rs/torrust-tracker) for more
Expand Down
2 changes: 1 addition & 1 deletion packages/configuration/src/v2_0_0/http_tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pub struct HttpTracker {
#[serde(default = "HttpTracker::default_tsl_config")]
pub tsl_config: Option<TslConfig>,

/// Weather the tracker should collect statistics about tracker usage.
/// Whether the tracker should collect statistics about tracker usage.
#[serde(default = "HttpTracker::default_tracker_usage_statistics")]
pub tracker_usage_statistics: bool,

Expand Down
73 changes: 73 additions & 0 deletions packages/configuration/src/v2_0_0/logging.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
//! Logging configuration and setup for `v2_0_0`.
//!
//! Contains the `Logging` configuration struct, the `Threshold` level enum,
//! the `TraceStyle` enum, and the `setup()` / `tracing_init()` helpers.
use std::sync::Once;

use serde::{Deserialize, Serialize};
use tracing::level_filters::LevelFilter;

static INIT: Once = Once::new();

#[allow(clippy::struct_excessive_bools)]
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
Expand Down Expand Up @@ -39,3 +48,67 @@ pub enum Threshold {
/// Corresponds to the `Trace` security level.
Trace,
}

/// Redirects log output to stdout at the threshold defined in the configuration.
pub fn setup(cfg: &Logging) {
let tracing_level = map_to_tracing_level_filter(&cfg.threshold);

if tracing_level == LevelFilter::OFF {
return;
}

INIT.call_once(|| {
tracing_init(tracing_level, &TraceStyle::Default);
});
}

fn map_to_tracing_level_filter(threshold: &Threshold) -> LevelFilter {
match threshold {
Threshold::Off => LevelFilter::OFF,
Threshold::Error => LevelFilter::ERROR,
Threshold::Warn => LevelFilter::WARN,
Threshold::Info => LevelFilter::INFO,
Threshold::Debug => LevelFilter::DEBUG,
Threshold::Trace => LevelFilter::TRACE,
}
}

fn tracing_init(filter: LevelFilter, style: &TraceStyle) {
let builder = tracing_subscriber::fmt()
.with_max_level(filter)
.with_ansi(true)
.with_test_writer();

let () = match style {
TraceStyle::Default => builder.init(),
TraceStyle::Pretty(display_filename) => builder.pretty().with_file(*display_filename).init(),
TraceStyle::Compact => builder.compact().init(),
TraceStyle::Json => builder.json().init(),
};

tracing::info!("Logging initialized");
}

#[derive(Debug)]
pub enum TraceStyle {
Default,
Pretty(bool),
Compact,
Json,
}

impl std::fmt::Display for TraceStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let style = match self {
TraceStyle::Default => "Default Style",
TraceStyle::Pretty(path) => match path {
true => "Pretty Style with File Paths",
false => "Pretty Style without File Paths",
},
TraceStyle::Compact => "Compact Style",
TraceStyle::Json => "Json Format",
};

f.write_str(style)
}
}
2 changes: 1 addition & 1 deletion packages/configuration/src/v2_0_0/udp_tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub struct UdpTracker {
#[serde(default = "UdpTracker::default_cookie_lifetime")]
pub cookie_lifetime: Duration,

/// Weather the tracker should collect statistics about tracker usage.
/// Whether the tracker should collect statistics about tracker usage.
#[serde(default = "UdpTracker::default_tracker_usage_statistics")]
pub tracker_usage_statistics: bool,

Expand Down
120 changes: 120 additions & 0 deletions packages/configuration/src/v3_0_0/core.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use serde::{Deserialize, Serialize};
use torrust_tracker_primitives::announce::AnnouncePolicy;
use torrust_tracker_primitives::{PrivateMode, TrackerPolicy};

use super::network::Network;
use crate::v3_0_0::database::Database;
use crate::validator::{SemanticValidationError, Validator};

#[allow(clippy::struct_excessive_bools)]
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
pub struct Core {
/// Announce policy configuration.
#[serde(default = "Core::default_announce_policy")]
pub announce_policy: AnnouncePolicy,

/// Database configuration.
#[serde(default = "Core::default_database")]
pub database: Database,

/// Interval in seconds that the cleanup job will run to remove inactive
/// peers from the torrent peer list.
#[serde(default = "Core::default_inactive_peer_cleanup_interval")]
pub inactive_peer_cleanup_interval: u64,

/// When `true` only approved torrents can be announced in the tracker.
#[serde(default = "Core::default_listed")]
pub listed: bool,

/// Network configuration.
#[serde(default = "Core::default_network")]
pub net: Network,

/// When `true` clients require a key to connect and use the tracker.
#[serde(default = "Core::default_private")]
pub private: bool,

/// Configuration specific when the tracker is running in private mode.
#[serde(default = "Core::default_private_mode")]
pub private_mode: Option<PrivateMode>,

/// Tracker policy configuration.
#[serde(default = "Core::default_tracker_policy")]
pub tracker_policy: TrackerPolicy,

/// Whether the tracker should collect statistics about tracker usage.
/// If enabled, the tracker will collect statistics like the number of
/// connections handled, the number of announce requests handled, etc.
/// Refer to the [`Tracker`](https://docs.rs/torrust-tracker) for more
/// information about the collected metrics.
#[serde(default = "Core::default_tracker_usage_statistics")]
pub tracker_usage_statistics: bool,
}

impl Default for Core {
fn default() -> Self {
Self {
announce_policy: Self::default_announce_policy(),
database: Self::default_database(),
inactive_peer_cleanup_interval: Self::default_inactive_peer_cleanup_interval(),
listed: Self::default_listed(),
net: Self::default_network(),
private: Self::default_private(),
private_mode: Self::default_private_mode(),
tracker_policy: Self::default_tracker_policy(),
tracker_usage_statistics: Self::default_tracker_usage_statistics(),
}
}
}

impl Core {
fn default_announce_policy() -> AnnouncePolicy {
AnnouncePolicy::default()
}

fn default_database() -> Database {
Database::default()
}

fn default_inactive_peer_cleanup_interval() -> u64 {
600
}

fn default_listed() -> bool {
false
}

fn default_network() -> Network {
Network::default()
}

fn default_private() -> bool {
false
}

fn default_private_mode() -> Option<PrivateMode> {
if Self::default_private() {
Some(PrivateMode::default())
} else {
None
}
}

fn default_tracker_policy() -> TrackerPolicy {
TrackerPolicy::default()
}

fn default_tracker_usage_statistics() -> bool {
true
}
}

impl Validator for Core {
fn validate(&self) -> Result<(), SemanticValidationError> {
if self.private_mode.is_some() && !self.private {
return Err(SemanticValidationError::UselessPrivateModeSection);
}

Ok(())
}
}
Loading
Loading