ncro/config: replace YAML configuration file with TOML

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ifb3cf9ad9747795b44eff1ee8cd538536a6a6964
This commit is contained in:
raf 2026-05-11 13:13:00 +03:00
commit 49545fdb6b
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
20 changed files with 280 additions and 199 deletions

1
.gitignore vendored
View file

@ -1,3 +1,2 @@
# Build output
/ncro
/target

75
Cargo.lock generated
View file

@ -1381,8 +1381,8 @@ dependencies = [
"hex",
"humantime-serde",
"serde",
"serde_yaml",
"thiserror 2.0.18",
"toml",
"url",
]
@ -2214,6 +2214,15 @@ dependencies = [
"serde_core",
]
[[package]]
name = "serde_spanned"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@ -2226,19 +2235,6 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "sha1"
version = "0.10.6"
@ -2769,6 +2765,45 @@ dependencies = [
"tokio",
]
[[package]]
name = "toml"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]]
name = "toml_datetime"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_parser"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
[[package]]
name = "tower"
version = "0.5.3"
@ -2935,12 +2970,6 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "untrusted"
version = "0.9.0"
@ -3487,6 +3516,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0"
[[package]]
name = "wit-bindgen"
version = "0.51.0"

View file

@ -1,5 +1,5 @@
[workspace]
members = [ "crates/*" ]
members = [ "crates/*", "ncro" ]
resolver = "3"
[workspace.package]
@ -8,6 +8,18 @@ license = "EUPL-1.2"
version = "2.0.0"
[workspace.dependencies]
# Workspace components
ncro-config = { path = "./crates/config", version = "2.0.0" }
ncro-db = { path = "./crates/db", version = "2.0.0" }
ncro-discovery = { path = "./crates/discovery", version = "2.0.0" }
ncro-health = { path = "./crates/health", version = "2.0.0" }
ncro-mesh = { path = "./crates/mesh", version = "2.0.0" }
ncro-metrics = { path = "./crates/metrics", version = "2.0.0" }
ncro-narinfo = { path = "./crates/narinfo", version = "2.0.0" }
ncro-router = { path = "./crates/router", version = "2.0.0" }
ncro-server = { path = "./crates/server", version = "2.0.0" }
# Other deps
anyhow = "1.0.102"
axum = "0.8.9"
base64 = "0.22.1"
@ -25,58 +37,20 @@ reqwest = { version = "0.13.3", default-features = false }
rmp-serde = "1.3.1"
serde = "1.0.228"
serde_json = "1.0.149"
serde_yaml = "0.9.34"
sqlx = { version = "0.8.6", default-features = false }
tempfile = "3.27.0"
thiserror = "2.0.18"
tokio = "1.52.3"
toml = "1.1.2"
tower = "0.5.3"
tower-http = "0.6.10"
tracing = "0.1.44"
tracing-subscriber = "0.3.23"
url = "2.5.8"
ncro-config = { path = "crates/config", version = "2.0.0" }
ncro-db = { path = "crates/db", version = "2.0.0" }
ncro-discovery = { path = "crates/discovery", version = "2.0.0" }
ncro-health = { path = "crates/health", version = "2.0.0" }
ncro-mesh = { path = "crates/mesh", version = "2.0.0" }
ncro-metrics = { path = "crates/metrics", version = "2.0.0" }
ncro-narinfo = { path = "crates/narinfo", version = "2.0.0" }
ncro-router = { path = "crates/router", version = "2.0.0" }
ncro-server = { path = "crates/server", version = "2.0.0" }
[package]
name = "ncro"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
anyhow.workspace = true
axum.workspace = true
clap = { workspace = true, features = [ "derive", "env" ] }
hex.workspace = true
tokio = { workspace = true, features = [ "macros", "rt-multi-thread", "signal", "time", "net", "fs" ] }
tracing.workspace = true
tracing-subscriber = { workspace = true, features = [ "env-filter", "json" ] }
ncro-config.workspace = true
ncro-db.workspace = true
ncro-discovery.workspace = true
ncro-health.workspace = true
ncro-mesh.workspace = true
ncro-metrics.workspace = true
ncro-router.workspace = true
ncro-server.workspace = true
[dev-dependencies]
tempfile.workspace = true
tower = { workspace = true, features = [ "util" ] }
# See:
# <https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html>
[lints.clippy]
[workspace.lints.clippy]
cargo = { level = "warn", priority = -1 }
complexity = { level = "warn", priority = -1 }
nursery = { level = "warn", priority = -1 }

View file

@ -55,7 +55,7 @@ measurements current and detect unhealthy upstreams.
$ ncro
# Point at a config file
$ ncro -config /etc/ncro/config.yaml
$ ncro --config /etc/ncro/config.toml
# Tell Nix to use it
$ nix-shell -p hello --substituters http://localhost:8080
@ -63,36 +63,38 @@ $ nix-shell -p hello --substituters http://localhost:8080
## Configuration
Default config is embedded; create a YAML file to override any field.
Default config is embedded; create a TOML file to override any field.
```yaml
server:
listen: ":8080"
read_timeout: 30s
write_timeout: 30s
```toml
[server]
listen = ":8080"
read_timeout = "30s"
write_timeout = "30s"
upstreams:
- url: "https://cache.nixos.org"
priority: 10 # lower = preferred on latency ties (within 10%)
- url: "https://nix-community.cachix.org"
priority: 20
[[upstreams]]
url = "https://cache.nixos.org"
priority = 10 # lower = preferred on latency ties (within 10%)
cache:
db_path: "/var/lib/ncro/routes.db"
max_entries: 100000 # LRU eviction above this
ttl: 1h # how long a routing decision is trusted
latency_alpha: 0.3 # EMA smoothing factor (0 < α < 1)
[[upstreams]]
url = "https://nix-community.cachix.org"
priority = 20
logging:
level: info # debug | info | warn | error
format: json # json | text
[cache]
db_path = "/var/lib/ncro/routes.db"
max_entries = 100000 # LRU eviction above this
ttl = "1h" # how long a routing decision is trusted
latency_alpha = 0.3 # EMA smoothing factor (0 < alpha < 1)
mesh:
enabled: false
bind_addr: "0.0.0.0:7946"
peers: [] # list of {addr, public_key} peer entries
private_key: "" # path to ed25519 key file; empty = ephemeral
gossip_interval: 30s
[logging]
level = "info" # debug | info | warn | error
format = "json" # json | text
[mesh]
enabled = false
bind_addr = "0.0.0.0:7946"
peers = [] # list of {addr, public_key} peer entries
private_key = "" # path to ed25519 key file; empty = ephemeral
gossip_interval = "30s"
```
### Environment Overrides
@ -132,7 +134,7 @@ Systemd service:
Description=Nix Cache Route Optimizer
[Service]
ExecStart=ncro --config /etc/ncro/config.yaml
ExecStart=ncro --config /etc/ncro/config.toml
DynamicUser=true
StateDirectory=ncro
Restart=on-failure
@ -157,15 +159,18 @@ Each peer entry takes an address and an optional ed25519 public key. When a
public key is provided, incoming gossip packets are verified against it; packets
from unlisted senders or with invalid signatures are silently dropped.
```yaml
mesh:
enabled: true
peers:
- addr: "100.64.1.2:7946"
public_key: "a1b2c3..." # hex-encoded ed25519 public key (32 bytes)
- addr: "100.64.1.3:7946"
public_key: "d4e5f6..."
private_key: "/var/lib/ncro/node.key"
```toml
[mesh]
enabled = true
private_key = "/var/lib/ncro/node.key"
[[mesh.peers]]
addr = "100.64.1.2:7946"
public_key = "a1b2c3..." # hex-encoded ed25519 public key (32 bytes)
[[mesh.peers]]
addr = "100.64.1.3:7946"
public_key = "d4e5f6..."
```
The node logs its public key on startup (`mesh node identity` log line). You

39
config.example.toml Normal file
View file

@ -0,0 +1,39 @@
[server]
listen = ":8080"
read_timeout = "30s"
write_timeout = "30s"
[[upstreams]]
url = "https://cache.nixos.org"
priority = 10
public_key = "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
# Try without a public key.
[[upstreams]]
url = "https://nix-community.cachix.org"
priority = 20
[cache]
db_path = "/var/lib/ncro/routes.db"
max_entries = 100000
ttl = "1h"
negative_ttl = "10m"
latency_alpha = 0.3
[discovery]
enabled = false
service_name = "_nix-serve._tcp"
domain = "local"
discovery_time = "5s"
priority = 20
[mesh]
enabled = false
bind_addr = "0.0.0.0:7946"
peers = []
private_key = "/etc/ncro/node.key"
gossip_interval = "30s"
[logging]
level = "info"
format = "json"

View file

@ -1,38 +0,0 @@
server:
listen: ":8080"
read_timeout: 30s
write_timeout: 30s
upstreams:
- url: "https://cache.nixos.org"
priority: 10
public_key: "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
# Try without a public key
- url: "https://nix-community.cachix.org"
priority: 20
cache:
db_path: "/var/lib/ncro/routes.db"
max_entries: 100000
ttl: 1h
negative_ttl: 10m
latency_alpha: 0.3
discovery:
enabled: false
service_name: "_nix-serve._tcp"
domain: "local"
discovery_time: 5s
priority: 20
mesh:
enabled: false
bind_addr: "0.0.0.0:7946"
peers: []
private_key: "/etc/ncro/node.key"
gossip_interval: 30s
logging:
level: "info"
format: "json"

View file

@ -1,13 +1,17 @@
[package]
name = "ncro-config"
name = "ncro-config"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
[dependencies]
hex.workspace = true
hex.workspace = true
humantime-serde.workspace = true
serde = { workspace = true, features = [ "derive" ] }
serde_yaml.workspace = true
thiserror.workspace = true
url.workspace = true
serde = { workspace = true, features = [ "derive" ] }
thiserror.workspace = true
toml.workspace = true
url.workspace = true
[lints]
workspace = true

View file

@ -9,7 +9,7 @@ pub enum ConfigError {
#[error("read config: {0}")]
Read(#[from] std::io::Error),
#[error("parse config: {0}")]
Parse(#[from] serde_yaml::Error),
Parse(#[from] toml::de::Error),
#[error("{0}")]
Validation(String),
}
@ -29,9 +29,9 @@ mod tests {
}
#[test]
fn parses_duration_yaml() -> Result<(), serde_yaml::Error> {
let cfg: Config = serde_yaml::from_str(
"server:\n read_timeout: 30s\ncache:\n ttl: 2h\n",
fn parses_duration_toml() -> Result<(), toml::de::Error> {
let cfg: Config = toml::from_str(
"[server]\nread_timeout = \"30s\"\n\n[cache]\nttl = \"2h\"\n",
)?;
assert_eq!(cfg.server.read_timeout.0, Duration::from_secs(30));
assert_eq!(cfg.cache.ttl.0, Duration::from_secs(7200));
@ -207,7 +207,7 @@ impl Config {
pub fn load(path: Option<&str>) -> Result<Self, ConfigError> {
let mut cfg = if let Some(path) = path.filter(|p| !p.is_empty()) {
let data = fs::read_to_string(path)?;
serde_yaml::from_str::<Self>(&data)?
toml::from_str::<Self>(&data)?
} else {
Self::default()
};

View file

@ -1,12 +1,16 @@
[package]
name = "ncro-db"
name = "ncro-db"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
[dependencies]
chrono = { workspace = true, features = [ "serde" ] }
serde = { workspace = true, features = [ "derive" ] }
sqlx = { workspace = true, features = [ "runtime-tokio-rustls", "sqlite", "macros", "migrate", "chrono" ] }
chrono = { workspace = true, features = [ "serde" ] }
serde = { workspace = true, features = [ "derive" ] }
sqlx = { workspace = true, features = [ "runtime-tokio-rustls", "sqlite", "macros", "migrate", "chrono" ] }
thiserror.workspace = true
tokio = { workspace = true, features = [ "fs" ] }
tokio = { workspace = true, features = [ "fs" ] }
[lints]
workspace = true

View file

@ -1,13 +1,17 @@
[package]
name = "ncro-discovery"
name = "ncro-discovery"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
[dependencies]
anyhow.workspace = true
mdns-sd.workspace = true
anyhow.workspace = true
mdns-sd.workspace = true
ncro-config.workspace = true
ncro-health.workspace = true
tokio = { workspace = true, features = [ "rt", "sync", "time" ] }
tracing.workspace = true
tokio = { workspace = true, features = [ "rt", "sync", "time" ] }
tracing.workspace = true
[lints]
workspace = true

View file

@ -1,10 +1,15 @@
[package]
name = "ncro-health"
name = "ncro-health"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
[dependencies]
ncro-config.workspace = true
reqwest = { workspace = true, features = [ "rustls" ] }
tokio = { workspace = true, features = [ "sync", "time", "rt" ] }
reqwest = { workspace = true, features = [ "rustls" ] }
tokio = { workspace = true, features = [ "sync", "time", "rt" ] }
[lints]
workspace = true

View file

@ -1,17 +1,21 @@
[package]
name = "ncro-mesh"
name = "ncro-mesh"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
[dependencies]
chrono.workspace = true
ed25519-dalek = { workspace = true, features = [ "rand_core" ] }
hex.workspace = true
ncro-db.workspace = true
rand.workspace = true
chrono.workspace = true
ed25519-dalek = { workspace = true, features = [ "rand_core" ] }
hex.workspace = true
ncro-db.workspace = true
rand.workspace = true
rmp-serde.workspace = true
serde = { workspace = true, features = [ "derive" ] }
serde = { workspace = true, features = [ "derive" ] }
thiserror.workspace = true
tokio = { workspace = true, features = [ "fs", "net", "rt", "sync", "time" ] }
tracing.workspace = true
tokio = { workspace = true, features = [ "fs", "net", "rt", "sync", "time" ] }
tracing.workspace = true
[lints]
workspace = true

View file

@ -1,8 +1,12 @@
[package]
name = "ncro-metrics"
name = "ncro-metrics"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
[dependencies]
prometheus.workspace = true
[lints]
workspace = true

View file

@ -1,14 +1,18 @@
[package]
name = "ncro-narinfo"
name = "ncro-narinfo"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
[dependencies]
base64.workspace = true
base64.workspace = true
ed25519-dalek.workspace = true
thiserror.workspace = true
thiserror.workspace = true
[dev-dependencies]
ed25519-dalek = { workspace = true, features = [ "rand_core" ] }
ed25519-dalek = { workspace = true, features = [ "rand_core" ] }
rand.workspace = true
[lints]
workspace = true

View file

@ -1,17 +1,21 @@
[package]
name = "ncro-router"
name = "ncro-router"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
[dependencies]
chrono.workspace = true
chrono.workspace = true
futures-util.workspace = true
ncro-db.workspace = true
ncro-health.workspace = true
ncro-db.workspace = true
ncro-health.workspace = true
ncro-metrics.workspace = true
ncro-narinfo.workspace = true
reqwest = { workspace = true, features = [ "rustls" ] }
thiserror.workspace = true
tokio = { workspace = true, features = [ "sync", "time", "rt" ] }
tracing.workspace = true
reqwest = { workspace = true, features = [ "rustls" ] }
thiserror.workspace = true
tokio = { workspace = true, features = [ "sync", "time", "rt" ] }
tracing.workspace = true
[lints]
workspace = true

View file

@ -1,18 +1,22 @@
[package]
name = "ncro-server"
name = "ncro-server"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
[dependencies]
axum = { workspace = true, features = [ "macros" ] }
bytes.workspace = true
axum = { workspace = true, features = [ "macros" ] }
bytes.workspace = true
futures-util.workspace = true
ncro-config.workspace = true
ncro-db.workspace = true
ncro-health.workspace = true
ncro-config.workspace = true
ncro-db.workspace = true
ncro-health.workspace = true
ncro-metrics.workspace = true
ncro-router.workspace = true
reqwest = { workspace = true, features = [ "rustls", "stream" ] }
serde = { workspace = true, features = [ "derive" ] }
tracing.workspace = true
ncro-router.workspace = true
reqwest = { workspace = true, features = [ "rustls", "stream" ] }
serde = { workspace = true, features = [ "derive" ] }
tracing.workspace = true
[lints]
workspace = true

30
ncro/Cargo.toml Normal file
View file

@ -0,0 +1,30 @@
[package]
name = "ncro"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
anyhow.workspace = true
axum.workspace = true
clap = { workspace = true, features = [ "derive", "env" ] }
hex.workspace = true
tokio = { workspace = true, features = [ "macros", "rt-multi-thread", "signal", "time", "net", "fs" ] }
tracing.workspace = true
tracing-subscriber = { workspace = true, features = [ "env-filter", "json" ] }
ncro-config.workspace = true
ncro-db.workspace = true
ncro-discovery.workspace = true
ncro-health.workspace = true
ncro-mesh.workspace = true
ncro-metrics.workspace = true
ncro-router.workspace = true
ncro-server.workspace = true
[dev-dependencies]
tempfile.workspace = true
tower = { workspace = true, features = [ "util" ] }
[lints]
workspace = true

View file

@ -7,10 +7,10 @@ self: {
inherit (lib.modules) mkIf;
inherit (lib.options) mkOption mkEnableOption mkPackageOption;
format = pkgs.formats.yaml {};
format = pkgs.formats.toml {};
cfg = config.services.ncro;
configFile = format.generate "ncro.yaml" cfg.settings;
configFile = format.generate "ncro.toml" cfg.settings;
in {
options.services.ncro = {
enable = mkEnableOption "ncro, the Nix cache route optimizer";
@ -22,7 +22,7 @@ in {
default = {};
description = ''
ncro configuration as an attribute set. Keys and structure match the
YAML config file format; all defaults are handled by the ncro binary.
TOML config file format; all defaults are handled by the ncro binary.
'';
example = {
logging.level = "info";