From 7f51d2fa1fb15c042313289276e8d4f9dce1b0e9 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 2 Feb 2025 22:39:45 -0500 Subject: [PATCH] Allow specifying configuration params in env vars (#83) --- Cargo.lock | 10 + Cargo.toml | 3 +- README.md | 12 +- docker-compose.yml | 5 +- server/Cargo.toml | 1 + server/src/bin/taskchampion-sync-server.rs | 204 +++++++++++++++++---- 6 files changed, 191 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 35b88c3..f2141e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1585,6 +1585,7 @@ dependencies = [ "serde_json", "taskchampion-sync-server-core", "taskchampion-sync-server-storage-sqlite", + "temp-env", "tempfile", "thiserror", "uuid", @@ -1617,6 +1618,15 @@ dependencies = [ "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]] name = "tempfile" version = "3.16.0" diff --git a/Cargo.toml b/Cargo.toml index 0b70d4a..0e201b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ thiserror = "2.0" futures = "^0.3.25" serde_json = "^1.0" 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" env_logger = "^0.11.5" rusqlite = { version = "0.32", features = ["bundled"] } @@ -22,3 +22,4 @@ chrono = { version = "^0.4.38", features = ["serde"] } actix-rt = "2" tempfile = "3" pretty_assertions = "1" +temp-env = "0.3" diff --git a/README.md b/README.md index 4bfbc1b..711304c 100644 --- a/README.md +++ b/README.md @@ -73,12 +73,18 @@ The server is configured with command-line options. See 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 -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, -such as when running a personal server, use `--allow-client-id `. +specify them in the environment variable `CLIENT_ID`, as a comma-separated list +of UUIDs. Client IDs can be specified with `--allow-client-id`, but this should +not be used on shared systems, as command line arguments are visible to all +users on the system. 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 diff --git a/docker-compose.yml b/docker-compose.yml index 86a22a1..0cd1e05 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,9 +56,10 @@ services: volume: nocopy: true subpath: tss - command: --data-dir /tss/taskchampion-sync-server --listen 0.0.0.0:8080 environment: - - RUST_LOG=info + - "RUST_LOG=info" + - "DATA_DIR=/tss/taskchampion-sync-server" + - "LISTEN=0.0.0.0:8080" depends_on: mkdir: condition: service_completed_successfully diff --git a/server/Cargo.toml b/server/Cargo.toml index 15ca6e1..fc7b53a 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -24,3 +24,4 @@ chrono.workspace = true actix-rt.workspace = true tempfile.workspace = true pretty_assertions.workspace = true +temp-env.workspace = true diff --git a/server/src/bin/taskchampion-sync-server.rs b/server/src/bin/taskchampion-sync-server.rs index ce3ddf3..349829d 100644 --- a/server/src/bin/taskchampion-sync-server.rs +++ b/server/src/bin/taskchampion-sync-server.rs @@ -23,29 +23,36 @@ fn command() -> Command { .arg( arg!(-l --listen
) .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()) + .env("LISTEN") .action(ArgAction::Append) .required(true), ) .arg( arg!(-d --"data-dir" "Directory in which to store data") .value_parser(ValueParser::os_string()) + .env("DATA_DIR") .default_value("/var/lib/taskchampion-sync-server"), ) .arg( arg!(-C --"allow-client-id" "Client IDs to allow (can be repeated; if not specified, all clients are allowed)") + .value_delimiter(',') .value_parser(value_parser!(Uuid)) + .env("CLIENT_ID") .action(ArgAction::Append) .required(false), ) .arg( arg!(--"snapshot-versions" "Target number of versions between snapshots") .value_parser(value_parser!(u32)) + .env("SNAPSHOT_VERSIONS") .default_value(default_snapshot_versions), ) .arg( arg!(--"snapshot-days" "Target number of days between snapshots") .value_parser(value_parser!(i64)) + .env("SNAPSHOT_DAYS") .default_value(default_snapshot_days), ) } @@ -95,6 +102,7 @@ mod test { use actix_web::{self, App}; use clap::ArgMatches; 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 fn allowed(matches: &ArgMatches) -> Option> { @@ -103,60 +111,180 @@ mod test { .map(|ids| ids.copied().collect::>()) } + #[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::("listen") + .unwrap() + .cloned() + .collect::>(), + 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::("listen") + .unwrap() + .cloned() + .collect::>(), + vec!["localhost:8080".to_string(), "otherhost:9090".to_string()] + ); + }); + } + #[test] fn command_allowed_client_ids_none() { - let matches = command().get_matches_from(["tss", "--listen", "localhost:8080"]); - assert_eq!(allowed(&matches), None); + with_var_unset("CLIENT_ID", || { + let matches = command().get_matches_from(["tss", "--listen", "localhost:8080"]); + assert_eq!(allowed(&matches), None); + }); } #[test] fn command_allowed_client_ids_one() { - let matches = command().get_matches_from([ - "tss", - "--listen", - "localhost:8080", - "-C", - "711d5cf3-0cf0-4eb8-9eca-6f7f220638c0", - ]); - assert_eq!( - allowed(&matches), - Some(vec![Uuid::parse_str( - "711d5cf3-0cf0-4eb8-9eca-6f7f220638c0" - ) - .unwrap()]) + with_var_unset("CLIENT_ID", || { + let matches = command().get_matches_from([ + "tss", + "--listen", + "localhost:8080", + "-C", + "711d5cf3-0cf0-4eb8-9eca-6f7f220638c0", + ]); + assert_eq!( + allowed(&matches), + Some(vec![Uuid::parse_str( + "711d5cf3-0cf0-4eb8-9eca-6f7f220638c0" + ) + .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] fn command_allowed_client_ids_two() { - let matches = command().get_matches_from([ - "tss", - "--listen", - "localhost:8080", - "-C", - "711d5cf3-0cf0-4eb8-9eca-6f7f220638c0", - "-C", - "bbaf4b61-344a-4a39-a19e-8caa0669b353", - ]); - assert_eq!( - allowed(&matches), - Some(vec![ - Uuid::parse_str("711d5cf3-0cf0-4eb8-9eca-6f7f220638c0").unwrap(), - Uuid::parse_str("bbaf4b61-344a-4a39-a19e-8caa0669b353").unwrap() - ]) + with_var_unset("CLIENT_ID", || { + let matches = command().get_matches_from([ + "tss", + "--listen", + "localhost:8080", + "-C", + "711d5cf3-0cf0-4eb8-9eca-6f7f220638c0", + "-C", + "bbaf4b61-344a-4a39-a19e-8caa0669b353", + ]); + 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] + 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] fn command_data_dir() { - let matches = command().get_matches_from([ - "tss", - "--data-dir", - "/foo/bar", - "--listen", - "localhost:8080", - ]); - assert_eq!(matches.get_one::("data-dir").unwrap(), "/foo/bar"); + with_var_unset("DATA_DIR", || { + let matches = command().get_matches_from([ + "tss", + "--data-dir", + "/foo/bar", + "--listen", + "localhost:8080", + ]); + assert_eq!(matches.get_one::("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::("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::("snapshot-days").unwrap(), 13i64); + assert_eq!(*matches.get_one::("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::("snapshot-days").unwrap(), 13i64); + assert_eq!(*matches.get_one::("snapshot-versions").unwrap(), 20u32); + }, + ); } #[actix_rt::test]