init config + storage
This commit is contained in:
Generated
+120
-11
@@ -190,6 +190,12 @@ version = "0.22.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.11.0"
|
version = "2.11.0"
|
||||||
@@ -356,16 +362,40 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc32fast"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-epoch"
|
||||||
|
version = "0.9.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossterm"
|
name = "crossterm"
|
||||||
version = "0.29.0"
|
version = "0.29.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
|
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"crossterm_winapi",
|
"crossterm_winapi",
|
||||||
"document-features",
|
"document-features",
|
||||||
"parking_lot",
|
"parking_lot 0.12.5",
|
||||||
"rustix",
|
"rustix",
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
@@ -534,6 +564,16 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fs2"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@@ -622,6 +662,15 @@ dependencies = [
|
|||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fxhash"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
@@ -1018,6 +1067,15 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "instant"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.12.0"
|
version = "2.12.0"
|
||||||
@@ -1211,7 +1269,7 @@ version = "0.28.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445"
|
checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"netlink-packet-core",
|
"netlink-packet-core",
|
||||||
@@ -1250,7 +1308,7 @@ version = "0.30.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -1383,6 +1441,17 @@ version = "4.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
|
checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
|
||||||
|
dependencies = [
|
||||||
|
"instant",
|
||||||
|
"lock_api",
|
||||||
|
"parking_lot_core 0.8.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@@ -1390,7 +1459,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lock_api",
|
"lock_api",
|
||||||
"parking_lot_core",
|
"parking_lot_core 0.9.12",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.8.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"instant",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall 0.2.16",
|
||||||
|
"smallvec",
|
||||||
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1401,7 +1484,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall 0.5.18",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
@@ -1645,13 +1728,22 @@ dependencies = [
|
|||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1781,7 +1873,7 @@ version = "1.1.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
@@ -2019,6 +2111,22 @@ version = "0.4.12"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sled"
|
||||||
|
version = "0.34.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935"
|
||||||
|
dependencies = [
|
||||||
|
"crc32fast",
|
||||||
|
"crossbeam-epoch",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"fs2",
|
||||||
|
"fxhash",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"parking_lot 0.11.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.15.1"
|
version = "1.15.1"
|
||||||
@@ -2402,7 +2510,7 @@ version = "0.6.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
@@ -2719,6 +2827,7 @@ dependencies = [
|
|||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sled",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -2868,7 +2977,7 @@ version = "0.244.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"hashbrown 0.15.5",
|
"hashbrown 0.15.5",
|
||||||
"indexmap 2.13.1",
|
"indexmap 2.13.1",
|
||||||
"semver",
|
"semver",
|
||||||
@@ -3216,7 +3325,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"indexmap 2.13.1",
|
"indexmap 2.13.1",
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -79,12 +79,18 @@ Control-plane also supports a config file at
|
|||||||
`/etc/wakey-control-plane/config.toml` (override with `--config-file`) so you
|
`/etc/wakey-control-plane/config.toml` (override with `--config-file`) so you
|
||||||
can persist telemetry settings instead of passing flags.
|
can persist telemetry settings instead of passing flags.
|
||||||
|
|
||||||
|
You can scaffold this file with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wakey-control-plane init-config
|
||||||
|
```
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
bind = "0.0.0.0:8080"
|
bind = "0.0.0.0:8080"
|
||||||
public_url = "https://cp.example.com"
|
public_url = "https://cp.example.com"
|
||||||
state_file = "/var/lib/wakey-control-plane/state.json"
|
state_file = "/var/lib/wakey-control-plane/state.db"
|
||||||
pid_file = "/var/run/wakey-control-plane.pid"
|
pid_file = "/var/run/wakey-control-plane.pid"
|
||||||
command_timeout_ms = 30000
|
command_timeout_ms = 30000
|
||||||
|
|
||||||
@@ -97,6 +103,9 @@ json_logs = false
|
|||||||
If `telemetry.otlp_endpoint` is omitted, logs still work normally and only local
|
If `telemetry.otlp_endpoint` is omitted, logs still work normally and only local
|
||||||
structured logs are emitted.
|
structured logs are emitted.
|
||||||
|
|
||||||
|
State is persisted in an embedded `sled` database (default
|
||||||
|
`/var/lib/wakey-control-plane/state.db`) rather than single-file JSON snapshots.
|
||||||
|
|
||||||
### Quick start
|
### Quick start
|
||||||
|
|
||||||
Control-plane:
|
Control-plane:
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ nix = { version = "0.30", default-features = false, features = ["signal", "proce
|
|||||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
sled = "0.34"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
tokio = { version = "1", features = [
|
tokio = { version = "1", features = [
|
||||||
"macros",
|
"macros",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use clap::{ArgAction, Args, Parser, Subcommand};
|
use clap::{ArgAction, Args, Parser, Subcommand};
|
||||||
|
|
||||||
pub const DEFAULT_STATE_FILE: &str = "/var/lib/wakey-control-plane/state.json";
|
pub const DEFAULT_STATE_FILE: &str = "/var/lib/wakey-control-plane/state.db";
|
||||||
pub const DEFAULT_PID_FILE: &str = "/var/run/wakey-control-plane.pid";
|
pub const DEFAULT_PID_FILE: &str = "/var/run/wakey-control-plane.pid";
|
||||||
pub const DEFAULT_CONFIG_FILE: &str = "/etc/wakey-control-plane/config.toml";
|
pub const DEFAULT_CONFIG_FILE: &str = "/etc/wakey-control-plane/config.toml";
|
||||||
|
|
||||||
@@ -22,6 +22,8 @@ pub struct Cli {
|
|||||||
pub enum Command {
|
pub enum Command {
|
||||||
/// Run the control-plane daemon.
|
/// Run the control-plane daemon.
|
||||||
Serve(ServeArgs),
|
Serve(ServeArgs),
|
||||||
|
/// Write a control-plane config scaffold.
|
||||||
|
InitConfig(InitConfigArgs),
|
||||||
/// Create a new enroll token for provisioning a router.
|
/// Create a new enroll token for provisioning a router.
|
||||||
IssueEnrollToken(IssueEnrollTokenArgs),
|
IssueEnrollToken(IssueEnrollTokenArgs),
|
||||||
/// Send SIGHUP to an already-running daemon.
|
/// Send SIGHUP to an already-running daemon.
|
||||||
@@ -52,6 +54,42 @@ pub struct ServeArgs {
|
|||||||
pub config_file: PathBuf,
|
pub config_file: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Clone)]
|
||||||
|
pub struct InitConfigArgs {
|
||||||
|
#[arg(long, default_value = DEFAULT_CONFIG_FILE)]
|
||||||
|
pub config_file: PathBuf,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub bind: Option<SocketAddr>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub public_url: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub state_file: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub pid_file: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub command_timeout_ms: Option<u64>,
|
||||||
|
|
||||||
|
#[arg(long = "enroll-token")]
|
||||||
|
pub enroll_tokens: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub telemetry_otlp_endpoint: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub telemetry_service_name: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub telemetry_json_logs: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub force: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub struct IssueEnrollTokenArgs {
|
pub struct IssueEnrollTokenArgs {
|
||||||
#[arg(long, default_value = DEFAULT_STATE_FILE)]
|
#[arg(long, default_value = DEFAULT_STATE_FILE)]
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ use std::path::PathBuf;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::cli::ServeArgs;
|
use crate::cli::{InitConfigArgs, ServeArgs};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DaemonConfig {
|
pub struct DaemonConfig {
|
||||||
@@ -54,6 +54,26 @@ struct FileTelemetryConfig {
|
|||||||
json_logs: Option<bool>,
|
json_logs: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct WritableConfig {
|
||||||
|
bind: String,
|
||||||
|
public_url: String,
|
||||||
|
state_file: PathBuf,
|
||||||
|
command_timeout_ms: u64,
|
||||||
|
pid_file: PathBuf,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
enroll_tokens: Vec<String>,
|
||||||
|
telemetry: WritableTelemetry,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct WritableTelemetry {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
otlp_endpoint: Option<String>,
|
||||||
|
service_name: String,
|
||||||
|
json_logs: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl DaemonConfig {
|
impl DaemonConfig {
|
||||||
pub fn from_serve_args(args: &ServeArgs) -> Result<Self> {
|
pub fn from_serve_args(args: &ServeArgs) -> Result<Self> {
|
||||||
let file = load_file_config(&args.config_file)?;
|
let file = load_file_config(&args.config_file)?;
|
||||||
@@ -157,3 +177,54 @@ pub fn issue_token_endpoint(base_url: &str) -> String {
|
|||||||
normalize_public_url(base_url)
|
normalize_public_url(base_url)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn write_init_config(args: &InitConfigArgs) -> Result<()> {
|
||||||
|
if args.config_file.exists() && !args.force {
|
||||||
|
anyhow::bail!(
|
||||||
|
"config {} already exists; re-run with --force to overwrite",
|
||||||
|
args.config_file.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bind = args
|
||||||
|
.bind
|
||||||
|
.unwrap_or_else(|| "0.0.0.0:8080".parse().expect("static default bind should parse"));
|
||||||
|
let public_url = normalize_public_url(
|
||||||
|
args.public_url
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("http://127.0.0.1:8080"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let body = WritableConfig {
|
||||||
|
bind: bind.to_string(),
|
||||||
|
public_url,
|
||||||
|
state_file: args
|
||||||
|
.state_file
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| PathBuf::from(crate::cli::DEFAULT_STATE_FILE)),
|
||||||
|
command_timeout_ms: args.command_timeout_ms.unwrap_or(30_000).max(1),
|
||||||
|
pid_file: args
|
||||||
|
.pid_file
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| PathBuf::from(crate::cli::DEFAULT_PID_FILE)),
|
||||||
|
enroll_tokens: args.enroll_tokens.clone(),
|
||||||
|
telemetry: WritableTelemetry {
|
||||||
|
otlp_endpoint: args.telemetry_otlp_endpoint.clone(),
|
||||||
|
service_name: args
|
||||||
|
.telemetry_service_name
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "wakey-control-plane".to_string()),
|
||||||
|
json_logs: args.telemetry_json_logs,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(parent) = args.config_file.parent() {
|
||||||
|
std::fs::create_dir_all(parent)
|
||||||
|
.with_context(|| format!("failed to create config dir {}", parent.display()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rendered = toml::to_string_pretty(&body).context("failed to render config template")?;
|
||||||
|
std::fs::write(&args.config_file, rendered)
|
||||||
|
.with_context(|| format!("failed to write config {}", args.config_file.display()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ async fn main() -> Result<()> {
|
|||||||
tracing::init(cli.verbose, &daemon.telemetry)?;
|
tracing::init(cli.verbose, &daemon.telemetry)?;
|
||||||
runtime::serve(daemon).await
|
runtime::serve(daemon).await
|
||||||
}
|
}
|
||||||
|
Command::InitConfig(args) => {
|
||||||
|
tracing::init(cli.verbose, &config::TelemetryConfig::default())?;
|
||||||
|
config::write_init_config(&args)?;
|
||||||
|
println!("config={}", args.config_file.display());
|
||||||
|
println!(
|
||||||
|
"next=wakey-control-plane serve --config-file {}",
|
||||||
|
args.config_file.display()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Command::IssueEnrollToken(args) => {
|
Command::IssueEnrollToken(args) => {
|
||||||
tracing::init(cli.verbose, &config::TelemetryConfig::default())?;
|
tracing::init(cli.verbose, &config::TelemetryConfig::default())?;
|
||||||
runtime::issue_enroll_token(args).await
|
runtime::issue_enroll_token(args).await
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::RwLock;
|
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -14,50 +13,59 @@ pub struct IssuedAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
struct PersistedState {
|
struct LegacyPersistedState {
|
||||||
enroll_tokens: HashSet<String>,
|
enroll_tokens: std::collections::HashSet<String>,
|
||||||
agents: HashMap<String, String>,
|
agents: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Store {
|
pub struct Store {
|
||||||
path: PathBuf,
|
db_path: PathBuf,
|
||||||
state: RwLock<PersistedState>,
|
enroll_tokens: sled::Tree,
|
||||||
|
agents: sled::Tree,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Store {
|
impl Store {
|
||||||
pub async fn load_or_init(path: &Path, enroll_tokens: Vec<String>) -> Result<Self> {
|
pub async fn load_or_init(path: &Path, enroll_tokens: Vec<String>) -> Result<Self> {
|
||||||
let seeded_tokens = enroll_tokens
|
let db_path = canonical_db_path(path);
|
||||||
.into_iter()
|
if let Some(parent) = db_path.parent() {
|
||||||
.map(|s| s.trim().to_string())
|
std::fs::create_dir_all(parent)
|
||||||
.filter(|s| !s.is_empty())
|
.with_context(|| format!("failed to create state dir {}", parent.display()))?;
|
||||||
.collect::<HashSet<_>>();
|
}
|
||||||
|
|
||||||
let initial = if path.exists() {
|
let db = sled::open(&db_path)
|
||||||
let raw = tokio::fs::read_to_string(path)
|
.with_context(|| format!("failed to open state db {}", db_path.display()))?;
|
||||||
.await
|
let enroll_tree = db
|
||||||
.with_context(|| format!("failed to read store {}", path.display()))?;
|
.open_tree("enroll_tokens")
|
||||||
debug!(path = %path.display(), bytes = raw.len(), "loading persisted control-plane store");
|
.context("failed to open enroll_tokens tree")?;
|
||||||
serde_json::from_str::<PersistedState>(&raw)
|
let agents_tree = db.open_tree("agents").context("failed to open agents tree")?;
|
||||||
.with_context(|| format!("failed to decode store {}", path.display()))?
|
|
||||||
} else {
|
|
||||||
info!(path = %path.display(), seeded_enroll_tokens = seeded_tokens.len(), "initializing new control-plane store");
|
|
||||||
PersistedState {
|
|
||||||
enroll_tokens: seeded_tokens,
|
|
||||||
agents: HashMap::new(),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let store = Self {
|
let store = Self {
|
||||||
path: path.to_path_buf(),
|
db_path,
|
||||||
state: RwLock::new(initial),
|
enroll_tokens: enroll_tree,
|
||||||
};
|
agents: agents_tree,
|
||||||
store.save().await?;
|
|
||||||
let (enroll_tokens, agents) = {
|
|
||||||
let snapshot = store.state.read().await;
|
|
||||||
(snapshot.enroll_tokens.len(), snapshot.agents.len())
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
store.maybe_migrate_legacy_json(path)?;
|
||||||
|
|
||||||
|
for token in enroll_tokens {
|
||||||
|
let token = token.trim();
|
||||||
|
if token.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
store
|
||||||
|
.enroll_tokens
|
||||||
|
.insert(token.as_bytes(), &[])
|
||||||
|
.with_context(|| format!("failed to seed enroll token into {}", store.db_path.display()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
store
|
||||||
|
.flush()
|
||||||
|
.with_context(|| format!("failed to flush state db {}", store.db_path.display()))?;
|
||||||
|
|
||||||
|
let enroll_tokens = store.enroll_tokens.iter().count();
|
||||||
|
let agents = store.agents.iter().count();
|
||||||
info!(
|
info!(
|
||||||
path = %store.path.display(),
|
path = %store.db_path.display(),
|
||||||
enroll_tokens,
|
enroll_tokens,
|
||||||
agents,
|
agents,
|
||||||
"control-plane store ready"
|
"control-plane store ready"
|
||||||
@@ -66,17 +74,23 @@ impl Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn enroll(&self, enroll_token: &str) -> Result<IssuedAgent> {
|
pub async fn enroll(&self, enroll_token: &str) -> Result<IssuedAgent> {
|
||||||
let mut state = self.state.write().await;
|
if self
|
||||||
if !state.enroll_tokens.remove(enroll_token) {
|
.enroll_tokens
|
||||||
|
.remove(enroll_token.as_bytes())
|
||||||
|
.context("failed removing enroll token")?
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
warn!("rejecting enroll attempt with invalid or consumed token");
|
warn!("rejecting enroll attempt with invalid or consumed token");
|
||||||
anyhow::bail!("invalid or already-used enroll token");
|
anyhow::bail!("invalid or already-used enroll token");
|
||||||
}
|
}
|
||||||
|
|
||||||
let agent_id = format!("agent-{}", Uuid::new_v4());
|
let agent_id = format!("agent-{}", Uuid::new_v4());
|
||||||
let agent_token = format!("tok-{}", Uuid::new_v4());
|
let agent_token = format!("tok-{}", Uuid::new_v4());
|
||||||
state.agents.insert(agent_id.clone(), agent_token.clone());
|
|
||||||
drop(state);
|
self.agents
|
||||||
self.save().await?;
|
.insert(agent_id.as_bytes(), agent_token.as_bytes())
|
||||||
|
.context("failed persisting agent credentials")?;
|
||||||
|
self.flush().context("failed flushing state db after enroll")?;
|
||||||
info!(agent_id = %agent_id, "issued persistent agent credentials");
|
info!(agent_id = %agent_id, "issued persistent agent credentials");
|
||||||
|
|
||||||
Ok(IssuedAgent {
|
Ok(IssuedAgent {
|
||||||
@@ -87,86 +101,90 @@ impl Store {
|
|||||||
|
|
||||||
pub async fn issue_enroll_token(&self) -> Result<String> {
|
pub async fn issue_enroll_token(&self) -> Result<String> {
|
||||||
let token = format!("enr-{}", Uuid::new_v4());
|
let token = format!("enr-{}", Uuid::new_v4());
|
||||||
let mut state = self.state.write().await;
|
self.enroll_tokens
|
||||||
state.enroll_tokens.insert(token.clone());
|
.insert(token.as_bytes(), &[])
|
||||||
drop(state);
|
.context("failed persisting enroll token")?;
|
||||||
self.save().await?;
|
self.flush()
|
||||||
|
.context("failed flushing state db after token issuance")?;
|
||||||
info!("persisted new enroll token");
|
info!("persisted new enroll token");
|
||||||
Ok(token)
|
Ok(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn reload_from_disk(&self) -> Result<()> {
|
pub async fn reload_from_disk(&self) -> Result<()> {
|
||||||
if !self.path.exists() {
|
// sled is durable and read-through; explicit reload is a no-op.
|
||||||
warn!(path = %self.path.display(), "reload requested but store file is missing");
|
info!(path = %self.db_path.display(), "reload requested; sled backend does not require in-memory reload");
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let raw = tokio::fs::read_to_string(&self.path)
|
|
||||||
.await
|
|
||||||
.with_context(|| format!("failed to read store {}", self.path.display()))?;
|
|
||||||
let decoded = serde_json::from_str::<PersistedState>(&raw)
|
|
||||||
.with_context(|| format!("failed to decode store {}", self.path.display()))?;
|
|
||||||
|
|
||||||
let enroll_tokens = decoded.enroll_tokens.len();
|
|
||||||
let agents = decoded.agents.len();
|
|
||||||
*self.state.write().await = decoded;
|
|
||||||
info!(path = %self.path.display(), enroll_tokens, agents, "reloaded control-plane store from disk");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn verify_agent_token(&self, agent_id: &str, token: &str) -> bool {
|
pub async fn verify_agent_token(&self, agent_id: &str, token: &str) -> bool {
|
||||||
self.state
|
match self.agents.get(agent_id.as_bytes()) {
|
||||||
.read()
|
Ok(Some(value)) => value.as_ref() == token.as_bytes(),
|
||||||
.await
|
Ok(None) => false,
|
||||||
.agents
|
Err(err) => {
|
||||||
.get(agent_id)
|
warn!(error = %err, agent_id = %agent_id, "failed to read agent token from state db");
|
||||||
.map(|stored| stored == token)
|
false
|
||||||
.unwrap_or(false)
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_agents(&self) -> Vec<String> {
|
pub async fn list_agents(&self) -> Vec<String> {
|
||||||
let mut out = self
|
let mut out = self
|
||||||
.state
|
|
||||||
.read()
|
|
||||||
.await
|
|
||||||
.agents
|
.agents
|
||||||
.keys()
|
.iter()
|
||||||
.cloned()
|
.filter_map(|item| item.ok())
|
||||||
|
.filter_map(|(key, _)| String::from_utf8(key.to_vec()).ok())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
out.sort();
|
out.sort();
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save(&self) -> Result<()> {
|
fn flush(&self) -> Result<()> {
|
||||||
let snapshot = self.state.read().await;
|
self.enroll_tokens
|
||||||
let body =
|
.flush()
|
||||||
serde_json::to_string_pretty(&*snapshot).context("failed to serialize store state")?;
|
.context("failed to flush enroll token tree")?;
|
||||||
let body_len = body.len();
|
self.agents
|
||||||
|
.flush()
|
||||||
|
.context("failed to flush agents tree")?;
|
||||||
|
debug!(path = %self.db_path.display(), "flushed sled state db");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(parent) = self.path.parent() {
|
fn maybe_migrate_legacy_json(&self, configured_path: &Path) -> Result<()> {
|
||||||
tokio::fs::create_dir_all(parent)
|
if configured_path.extension().and_then(|x| x.to_str()) != Some("json") {
|
||||||
.await
|
return Ok(());
|
||||||
.with_context(|| format!("failed to create store dir {}", parent.display()))?;
|
}
|
||||||
|
if self.enroll_tokens.iter().next().is_some() || self.agents.iter().next().is_some() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if !configured_path.exists() {
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let tmp = self.path.with_extension("json.tmp");
|
let raw = std::fs::read_to_string(configured_path)
|
||||||
tokio::fs::write(&tmp, body)
|
.with_context(|| format!("failed to read legacy state {}", configured_path.display()))?;
|
||||||
.await
|
let legacy: LegacyPersistedState = serde_json::from_str(&raw)
|
||||||
.with_context(|| format!("failed to write temp store {}", tmp.display()))?;
|
.with_context(|| format!("failed to parse legacy state {}", configured_path.display()))?;
|
||||||
tokio::fs::rename(&tmp, &self.path).await.with_context(|| {
|
|
||||||
format!(
|
for token in legacy.enroll_tokens {
|
||||||
"failed to atomically move temp store {} into {}",
|
self.enroll_tokens
|
||||||
tmp.display(),
|
.insert(token.as_bytes(), &[])
|
||||||
self.path.display()
|
.context("failed to migrate enroll token")?;
|
||||||
)
|
}
|
||||||
})?;
|
for (agent_id, token) in legacy.agents {
|
||||||
debug!(
|
self.agents
|
||||||
path = %self.path.display(),
|
.insert(agent_id.as_bytes(), token.as_bytes())
|
||||||
bytes = body_len,
|
.context("failed to migrate agent token")?;
|
||||||
enroll_tokens = snapshot.enroll_tokens.len(),
|
}
|
||||||
agents = snapshot.agents.len(),
|
|
||||||
"saved control-plane store"
|
self.flush().context("failed flushing migrated legacy state")?;
|
||||||
);
|
info!(legacy = %configured_path.display(), db = %self.db_path.display(), "migrated legacy json state into sled db");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn canonical_db_path(configured: &Path) -> PathBuf {
|
||||||
|
match configured.extension().and_then(|x| x.to_str()) {
|
||||||
|
Some("json") => configured.with_extension("db"),
|
||||||
|
_ => configured.to_path_buf(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user