Allow specifying configuration params in env vars

This commit is contained in:
Dustin J. Mitchell 2025-02-01 16:31:51 -05:00
parent 5ffd179dcc
commit 7dec5fe1af
No known key found for this signature in database
6 changed files with 189 additions and 43 deletions

10
Cargo.lock generated
View file

@ -1585,6 +1585,7 @@ dependencies = [
"serde_json", "serde_json",
"taskchampion-sync-server-core", "taskchampion-sync-server-core",
"taskchampion-sync-server-storage-sqlite", "taskchampion-sync-server-storage-sqlite",
"temp-env",
"tempfile", "tempfile",
"thiserror", "thiserror",
"uuid", "uuid",
@ -1617,6 +1618,15 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "temp-env"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96374855068f47402c3121c6eed88d29cb1de8f3ab27090e273e420bdabcf050"
dependencies = [
"parking_lot",
]
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.16.0" version = "3.16.0"

View file

@ -14,7 +14,7 @@ thiserror = "2.0"
futures = "^0.3.25" futures = "^0.3.25"
serde_json = "^1.0" serde_json = "^1.0"
serde = { version = "^1.0.147", features = ["derive"] } serde = { version = "^1.0.147", features = ["derive"] }
clap = { version = "^4.5.6", features = ["string"] } clap = { version = "^4.5.6", features = ["string", "env"] }
log = "^0.4.17" log = "^0.4.17"
env_logger = "^0.11.5" env_logger = "^0.11.5"
rusqlite = { version = "0.32", features = ["bundled"] } rusqlite = { version = "0.32", features = ["bundled"] }
@ -22,3 +22,4 @@ chrono = { version = "^0.4.38", features = ["serde"] }
actix-rt = "2" actix-rt = "2"
tempfile = "3" tempfile = "3"
pretty_assertions = "1" pretty_assertions = "1"
temp-env = "0.3"

View file

@ -73,12 +73,17 @@ The server is configured with command-line options. See
The `--listen` option specifies the interface and port the server listens on. The `--listen` option specifies the interface and port the server listens on.
It must contain an IP-Address or a DNS name and a port number. This option is It must contain an IP-Address or a DNS name and a port number. This option is
mandatory, but can be repeated to specify multiple interfaces or ports. mandatory, but can be repeated to specify multiple interfaces or ports. This
value can be specified in environment variable `LISTEN`, as a comma-separated
list of values.
The `--data-dir` option specifies where the server should store its data. The `--data-dir` option specifies where the server should store its data. This
value can be specified in the environment variable `DATA_DIR`.
By default, the server allows all client IDs. To limit the accepted client IDs, By default, the server allows all client IDs. To limit the accepted client IDs,
such as when running a personal server, use `--allow-client-id <client-id>`. such as when running a personal server, use `--allow-client-id <client-id>`.
This value can be specified in the environment variable `CLIENT_ID`, as a
comma-separated list of client IDs.
The server only logs errors by default. To add additional logging output, set The server only logs errors by default. To add additional logging output, set
environment variable `RUST_LOG` to `info` to get a log message for every environment variable `RUST_LOG` to `info` to get a log message for every

View file

@ -56,9 +56,10 @@ services:
volume: volume:
nocopy: true nocopy: true
subpath: tss subpath: tss
command: --data-dir /tss/taskchampion-sync-server --listen 0.0.0.0:8080
environment: environment:
- RUST_LOG=info - "RUST_LOG=info"
- "DATA_DIR=/tss/taskchampion-sync-server"
- "LISTEN=0.0.0.0:8080"
depends_on: depends_on:
mkdir: mkdir:
condition: service_completed_successfully condition: service_completed_successfully

View file

@ -24,3 +24,4 @@ chrono.workspace = true
actix-rt.workspace = true actix-rt.workspace = true
tempfile.workspace = true tempfile.workspace = true
pretty_assertions.workspace = true pretty_assertions.workspace = true
temp-env.workspace = true

View file

@ -23,29 +23,36 @@ fn command() -> Command {
.arg( .arg(
arg!(-l --listen <ADDRESS>) arg!(-l --listen <ADDRESS>)
.help("Address and Port on which to listen on. Can be an IP Address or a DNS name followed by a colon and a port e.g. localhost:8080") .help("Address and Port on which to listen on. Can be an IP Address or a DNS name followed by a colon and a port e.g. localhost:8080")
.value_delimiter(',')
.value_parser(ValueParser::string()) .value_parser(ValueParser::string())
.env("LISTEN")
.action(ArgAction::Append) .action(ArgAction::Append)
.required(true), .required(true),
) )
.arg( .arg(
arg!(-d --"data-dir" <DIR> "Directory in which to store data") arg!(-d --"data-dir" <DIR> "Directory in which to store data")
.value_parser(ValueParser::os_string()) .value_parser(ValueParser::os_string())
.env("DATA_DIR")
.default_value("/var/lib/taskchampion-sync-server"), .default_value("/var/lib/taskchampion-sync-server"),
) )
.arg( .arg(
arg!(-C --"allow-client-id" <CLIENT_ID> "Client IDs to allow (can be repeated; if not specified, all clients are allowed)") arg!(-C --"allow-client-id" <CLIENT_ID> "Client IDs to allow (can be repeated; if not specified, all clients are allowed)")
.value_delimiter(',')
.value_parser(value_parser!(Uuid)) .value_parser(value_parser!(Uuid))
.env("CLIENT_ID")
.action(ArgAction::Append) .action(ArgAction::Append)
.required(false), .required(false),
) )
.arg( .arg(
arg!(--"snapshot-versions" <NUM> "Target number of versions between snapshots") arg!(--"snapshot-versions" <NUM> "Target number of versions between snapshots")
.value_parser(value_parser!(u32)) .value_parser(value_parser!(u32))
.env("SNAPSHOT_VERSIONS")
.default_value(default_snapshot_versions), .default_value(default_snapshot_versions),
) )
.arg( .arg(
arg!(--"snapshot-days" <NUM> "Target number of days between snapshots") arg!(--"snapshot-days" <NUM> "Target number of days between snapshots")
.value_parser(value_parser!(i64)) .value_parser(value_parser!(i64))
.env("SNAPSHOT_DAYS")
.default_value(default_snapshot_days), .default_value(default_snapshot_days),
) )
} }
@ -95,6 +102,7 @@ mod test {
use actix_web::{self, App}; use actix_web::{self, App};
use clap::ArgMatches; use clap::ArgMatches;
use taskchampion_sync_server_core::InMemoryStorage; use taskchampion_sync_server_core::InMemoryStorage;
use temp_env::{with_var, with_var_unset, with_vars, with_vars_unset};
/// Get the list of allowed client IDs /// Get the list of allowed client IDs
fn allowed(matches: &ArgMatches) -> Option<Vec<Uuid>> { fn allowed(matches: &ArgMatches) -> Option<Vec<Uuid>> {
@ -103,60 +111,180 @@ mod test {
.map(|ids| ids.copied().collect::<Vec<_>>()) .map(|ids| ids.copied().collect::<Vec<_>>())
} }
#[test]
fn command_listen_two() {
with_var_unset("LISTEN", || {
let matches = command().get_matches_from([
"tss",
"--listen",
"localhost:8080",
"--listen",
"otherhost:9090",
]);
assert_eq!(
matches
.get_many::<String>("listen")
.unwrap()
.cloned()
.collect::<Vec<String>>(),
vec!["localhost:8080".to_string(), "otherhost:9090".to_string()]
);
});
}
#[test]
fn command_listen_two_env() {
with_var("LISTEN", Some("localhost:8080,otherhost:9090"), || {
let matches = command().get_matches_from(["tss"]);
assert_eq!(
matches
.get_many::<String>("listen")
.unwrap()
.cloned()
.collect::<Vec<String>>(),
vec!["localhost:8080".to_string(), "otherhost:9090".to_string()]
);
});
}
#[test] #[test]
fn command_allowed_client_ids_none() { fn command_allowed_client_ids_none() {
let matches = command().get_matches_from(["tss", "--listen", "localhost:8080"]); with_var_unset("CLIENT_ID", || {
assert_eq!(allowed(&matches), None); let matches = command().get_matches_from(["tss", "--listen", "localhost:8080"]);
assert_eq!(allowed(&matches), None);
});
} }
#[test] #[test]
fn command_allowed_client_ids_one() { fn command_allowed_client_ids_one() {
let matches = command().get_matches_from([ with_var_unset("CLIENT_ID", || {
"tss", let matches = command().get_matches_from([
"--listen", "tss",
"localhost:8080", "--listen",
"-C", "localhost:8080",
"711d5cf3-0cf0-4eb8-9eca-6f7f220638c0", "-C",
]); "711d5cf3-0cf0-4eb8-9eca-6f7f220638c0",
assert_eq!( ]);
allowed(&matches), assert_eq!(
Some(vec![Uuid::parse_str( allowed(&matches),
"711d5cf3-0cf0-4eb8-9eca-6f7f220638c0" Some(vec![Uuid::parse_str(
) "711d5cf3-0cf0-4eb8-9eca-6f7f220638c0"
.unwrap()]) )
.unwrap()])
);
});
}
#[test]
fn command_allowed_client_ids_one_env() {
with_var(
"CLIENT_ID",
Some("711d5cf3-0cf0-4eb8-9eca-6f7f220638c0"),
|| {
let matches = command().get_matches_from(["tss", "--listen", "localhost:8080"]);
assert_eq!(
allowed(&matches),
Some(vec![Uuid::parse_str(
"711d5cf3-0cf0-4eb8-9eca-6f7f220638c0"
)
.unwrap()])
);
},
); );
} }
#[test] #[test]
fn command_allowed_client_ids_two() { fn command_allowed_client_ids_two() {
let matches = command().get_matches_from([ with_var_unset("CLIENT_ID", || {
"tss", let matches = command().get_matches_from([
"--listen", "tss",
"localhost:8080", "--listen",
"-C", "localhost:8080",
"711d5cf3-0cf0-4eb8-9eca-6f7f220638c0", "-C",
"-C", "711d5cf3-0cf0-4eb8-9eca-6f7f220638c0",
"bbaf4b61-344a-4a39-a19e-8caa0669b353", "-C",
]); "bbaf4b61-344a-4a39-a19e-8caa0669b353",
assert_eq!( ]);
allowed(&matches), assert_eq!(
Some(vec![ allowed(&matches),
Uuid::parse_str("711d5cf3-0cf0-4eb8-9eca-6f7f220638c0").unwrap(), Some(vec![
Uuid::parse_str("bbaf4b61-344a-4a39-a19e-8caa0669b353").unwrap() Uuid::parse_str("711d5cf3-0cf0-4eb8-9eca-6f7f220638c0").unwrap(),
]) Uuid::parse_str("bbaf4b61-344a-4a39-a19e-8caa0669b353").unwrap()
])
);
});
}
#[test]
fn command_allowed_client_ids_two_env() {
with_var(
"CLIENT_ID",
Some("711d5cf3-0cf0-4eb8-9eca-6f7f220638c0,bbaf4b61-344a-4a39-a19e-8caa0669b353"),
|| {
let matches = command().get_matches_from(["tss", "--listen", "localhost:8080"]);
assert_eq!(
allowed(&matches),
Some(vec![
Uuid::parse_str("711d5cf3-0cf0-4eb8-9eca-6f7f220638c0").unwrap(),
Uuid::parse_str("bbaf4b61-344a-4a39-a19e-8caa0669b353").unwrap()
])
);
},
); );
} }
#[test] #[test]
fn command_data_dir() { fn command_data_dir() {
let matches = command().get_matches_from([ with_var_unset("DATA_DIR", || {
"tss", let matches = command().get_matches_from([
"--data-dir", "tss",
"/foo/bar", "--data-dir",
"--listen", "/foo/bar",
"localhost:8080", "--listen",
]); "localhost:8080",
assert_eq!(matches.get_one::<OsString>("data-dir").unwrap(), "/foo/bar"); ]);
assert_eq!(matches.get_one::<OsString>("data-dir").unwrap(), "/foo/bar");
});
}
#[test]
fn command_data_dir_env() {
with_var("DATA_DIR", Some("/foo/bar"), || {
let matches = command().get_matches_from(["tss", "--listen", "localhost:8080"]);
assert_eq!(matches.get_one::<OsString>("data-dir").unwrap(), "/foo/bar");
});
}
#[test]
fn command_snapshot() {
with_vars_unset(["SNAPSHOT_DAYS", "SNAPSHOT_VERSIONS"], || {
let matches = command().get_matches_from([
"tss",
"--listen",
"localhost:8080",
"--snapshot-days",
"13",
"--snapshot-versions",
"20",
]);
assert_eq!(*matches.get_one::<i64>("snapshot-days").unwrap(), 13i64);
assert_eq!(*matches.get_one::<u32>("snapshot-versions").unwrap(), 20u32);
});
}
#[test]
fn command_snapshot_env() {
with_vars(
[
("SNAPSHOT_DAYS", Some("13")),
("SNAPSHOT_VERSIONS", Some("20")),
],
|| {
let matches = command().get_matches_from(["tss", "--listen", "localhost:8080"]);
assert_eq!(*matches.get_one::<i64>("snapshot-days").unwrap(), 13i64);
assert_eq!(*matches.get_one::<u32>("snapshot-versions").unwrap(), 20u32);
},
);
} }
#[actix_rt::test] #[actix_rt::test]