Implement basic functionality
I've been working on this off-and-on for a few days without committing
anything 🤨. With this commit, the basic functionality for writing
configuration files from _etcd_ keys is in place. The daemon connects
to _etcd_ on startup, reads all currently-populated keys, and then
enters a watch loop. It gracefully handles being disconnected from
_etcd_ and will reconnect with a backoff timer. In addition to writing
file contents, the daemon can set file permissions and run commands
after changing files.
main
parent
f1160846db
commit
da756ff1a4
|
@ -1 +1,2 @@
|
|||
/target
|
||||
config.toml
|
||||
|
|
|
@ -51,7 +51,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -62,7 +62,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -151,6 +151,24 @@ version = "2.6.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.6.1"
|
||||
|
@ -169,6 +187,63 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive-adhoc"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5283ac2881753c76c0892406705553f0d9ab30649f81e18964d3408f4501edb8"
|
||||
dependencies = [
|
||||
"derive-adhoc-macros",
|
||||
"heck 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive-adhoc-macros"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c21b673a9b8c78c34908e6fcb42b922e11c4df2de5237f1c3f58d3285904a84b"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"itertools 0.11.0",
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"sha3",
|
||||
"strum",
|
||||
"syn 1.0.109",
|
||||
"void",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.13.0"
|
||||
|
@ -213,6 +288,15 @@ version = "2.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
|
||||
|
||||
[[package]]
|
||||
name = "file-mode"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773ea145485772b8d354624b32adbe20e776353d3e48c7b03ef44e3455e9815c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.4.2"
|
||||
|
@ -225,6 +309,15 @@ version = "1.0.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.30"
|
||||
|
@ -264,6 +357,16 @@ dependencies = [
|
|||
"pin-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.15"
|
||||
|
@ -312,6 +415,12 @@ version = "0.14.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
|
@ -324,6 +433,23 @@ version = "0.3.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hostname"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.1.0"
|
||||
|
@ -424,6 +550,16 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
|
@ -444,6 +580,15 @@ dependencies = [
|
|||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
|
@ -459,6 +604,15 @@ version = "1.0.11"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "keccak"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654"
|
||||
dependencies = [
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
|
@ -471,6 +625,12 @@ version = "0.2.155"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||
|
||||
[[package]]
|
||||
name = "libyml"
|
||||
version = "0.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64804cc6a5042d4f05379909ba25b503ec04e2c082151d62122d5dcaa274b961"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.14"
|
||||
|
@ -487,10 +647,23 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
|||
name = "luci"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"blake2",
|
||||
"etcd-client",
|
||||
"file-mode",
|
||||
"hex",
|
||||
"hostname",
|
||||
"pwd-grp",
|
||||
"sd-notify",
|
||||
"serde",
|
||||
"serde_yml",
|
||||
"shlex",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -578,6 +751,12 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
|
@ -611,7 +790,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -639,7 +818,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"toml_edit 0.19.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -668,8 +857,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "5bb182580f71dd070f88d01ce3de9f4da5021db7115d2e1c3605a754153b77c1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"heck",
|
||||
"itertools",
|
||||
"heck 0.5.0",
|
||||
"itertools 0.13.0",
|
||||
"log",
|
||||
"multimap",
|
||||
"once_cell",
|
||||
|
@ -678,7 +867,7 @@ dependencies = [
|
|||
"prost",
|
||||
"prost-types",
|
||||
"regex",
|
||||
"syn",
|
||||
"syn 2.0.72",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
|
@ -689,10 +878,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "18bec9b0adc4eba778b33684b7ba3e7137789434769ee3ce3930463ef904cfca"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
"itertools 0.13.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -704,6 +893,18 @@ dependencies = [
|
|||
"prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pwd-grp"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6955c41fd7e4283bdf6ff3e7218b7e3f8ef24c4236b31d22be050f4cfd5e2a2c"
|
||||
dependencies = [
|
||||
"derive-adhoc",
|
||||
"libc",
|
||||
"paste",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.36"
|
||||
|
@ -869,6 +1070,18 @@ version = "1.0.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "sd-notify"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4646d6f919800cd25c50edb49438a1381e2cd4833c027e75e8897981c50b8b5e"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.204"
|
||||
|
@ -886,7 +1099,54 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.120"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yml"
|
||||
version = "0.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48e76bab63c3fd98d27c17f9cbce177f64a91f5e69ac04cafe04e1bb25d1dc3c"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"itoa",
|
||||
"libyml",
|
||||
"log",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha3"
|
||||
version = "0.10.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -898,6 +1158,12 @@ dependencies = [
|
|||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.2"
|
||||
|
@ -938,12 +1204,45 @@ version = "0.9.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.25.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.72"
|
||||
|
@ -979,6 +1278,26 @@ dependencies = [
|
|||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.8"
|
||||
|
@ -989,6 +1308,21 @@ dependencies = [
|
|||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.39.1"
|
||||
|
@ -1014,7 +1348,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1052,6 +1386,51 @@ dependencies = [
|
|||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.22.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.19.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"toml_datetime",
|
||||
"winnow 0.5.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow 0.6.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tonic"
|
||||
version = "0.12.1"
|
||||
|
@ -1094,7 +1473,7 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
"prost-build",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1148,7 +1527,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1196,24 +1575,69 @@ version = "0.2.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "void"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
|
@ -1251,6 +1675,25 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
|
@ -1324,6 +1767,24 @@ version = "0.52.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "557404e450152cd6795bb558bca69e43c585055f4606e3bcae5894fc6dac9ba0"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.1"
|
||||
|
|
15
Cargo.toml
15
Cargo.toml
|
@ -4,7 +4,20 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22.1"
|
||||
blake2 = "0.10.6"
|
||||
etcd-client = { version = "0.14.0", features = ["tls"] }
|
||||
tokio = { version = "1.39.1", default-features = false, features = ["rt", "macros", "rt-multi-thread", "signal"] }
|
||||
file-mode = "0.1.2"
|
||||
hex = "0.4.3"
|
||||
hostname = "0.4.0"
|
||||
pwd-grp = "0.1.1"
|
||||
sd-notify = "0.4.2"
|
||||
serde = { version = "1.0.204", features = ["derive"] }
|
||||
serde_yml = "0.0.11"
|
||||
shlex = "1.3.0"
|
||||
thiserror = "1.0.63"
|
||||
tokio = { version = "1.39.1", default-features = false, features = ["rt", "macros", "rt-multi-thread", "signal", "process"] }
|
||||
toml = "0.8.15"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
url = { version = "2.5.2", features = ["serde"] }
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
max_width = 79
|
|
@ -0,0 +1,48 @@
|
|||
use std::iter::Iterator;
|
||||
use std::time::Duration;
|
||||
|
||||
use tracing::warn;
|
||||
|
||||
pub struct Backoff {
|
||||
duration: Duration,
|
||||
max: Duration,
|
||||
min: Duration,
|
||||
}
|
||||
|
||||
impl Default for Backoff {
|
||||
fn default() -> Self {
|
||||
let min = Duration::from_millis(250);
|
||||
let max = Duration::from_secs(300);
|
||||
let duration = min;
|
||||
Self { duration, max, min }
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Backoff {
|
||||
type Item = Duration;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let duration = self.duration;
|
||||
self.duration = std::cmp::min(self.duration * 2, self.max);
|
||||
Some(duration)
|
||||
}
|
||||
}
|
||||
|
||||
impl Backoff {
|
||||
#[allow(dead_code)]
|
||||
pub fn reset(&mut self) {
|
||||
self.duration = self.min;
|
||||
}
|
||||
|
||||
pub async fn sleep(&mut self) {
|
||||
warn!(
|
||||
"Retrying in {}",
|
||||
if self.duration.as_secs() < 1 {
|
||||
format!("{} ms", self.duration.as_millis())
|
||||
} else {
|
||||
format!("{} seconds", self.duration.as_secs())
|
||||
}
|
||||
);
|
||||
tokio::time::sleep(self.next().unwrap()).await;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
use std::io::ErrorKind;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::Deserialize;
|
||||
use tracing::{debug, info};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Unable to read configuration file: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("Failed to parse configuration file: {0}")]
|
||||
Toml(#[from] toml::de::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct EtcdConfig {
|
||||
#[serde(default = "default_etcd_hosts")]
|
||||
pub hosts: Vec<Url>,
|
||||
|
||||
pub client_cert: Option<PathBuf>,
|
||||
pub client_key: Option<PathBuf>,
|
||||
pub ca_cert: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Configuration {
|
||||
#[serde(default = "default_namespace")]
|
||||
pub namespace: String,
|
||||
#[serde(default)]
|
||||
pub path_prefix: PathBuf,
|
||||
pub etcd: EtcdConfig,
|
||||
}
|
||||
|
||||
impl Default for EtcdConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
hosts: default_etcd_hosts(),
|
||||
|
||||
client_cert: None,
|
||||
client_key: None,
|
||||
ca_cert: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Configuration {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
namespace: default_namespace(),
|
||||
path_prefix: Default::default(),
|
||||
etcd: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Configuration {
|
||||
pub fn load(path: Option<impl AsRef<Path>>) -> Result<Self, Error> {
|
||||
let path = path
|
||||
.map(|p| p.as_ref().into())
|
||||
.unwrap_or_else(Self::default_path);
|
||||
debug!("Loading configuration from {}", path.display());
|
||||
let config = match std::fs::read_to_string(&path) {
|
||||
Ok(d) => toml::from_str(&d)?,
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => {
|
||||
debug!("{}: {}", path.display(), e);
|
||||
Default::default()
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
match std::env::var("DUMP_CONFIG") {
|
||||
Ok(s) if s == "1" => info!("{:?}", config),
|
||||
Ok(_) | Err(_) => (),
|
||||
};
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn default_path() -> PathBuf {
|
||||
match std::env::var_os(format!(
|
||||
"{}_CONFIG",
|
||||
env!("CARGO_PKG_NAME").to_uppercase()
|
||||
)) {
|
||||
Some(v) => PathBuf::from(v),
|
||||
None => match std::env::var_os("XDG_CONFIG_HOME") {
|
||||
Some(v) => {
|
||||
let mut p = PathBuf::from(v);
|
||||
p.push(env!("CARGO_PKG_NAME"));
|
||||
p.push("config.toml");
|
||||
p
|
||||
}
|
||||
None => match std::env::var_os("HOME") {
|
||||
Some(v) => {
|
||||
let mut p = PathBuf::from(v);
|
||||
p.push(".config");
|
||||
p.push(env!("CARGO_PKG_NAME"));
|
||||
p.push("config.toml");
|
||||
p
|
||||
}
|
||||
None => PathBuf::from("config.toml"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_namespace() -> String {
|
||||
env!("CARGO_PKG_NAME").into()
|
||||
}
|
||||
|
||||
fn default_etcd_hosts() -> Vec<Url> {
|
||||
vec![Url::parse("http://localhost:2379").unwrap()]
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
use etcd_client::{
|
||||
Certificate, Client, ConnectOptions, GetOptions, Identity, KeyValue,
|
||||
TlsOptions, WatchOptions,
|
||||
};
|
||||
use sd_notify::NotifyState;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::{debug, error, info, trace, warn};
|
||||
|
||||
use crate::backoff::Backoff;
|
||||
use crate::config::Configuration;
|
||||
use crate::model::ContentError;
|
||||
use crate::processor::{Processor, Sender};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum WatchError {
|
||||
#[error("Could not get system hostname: {0}")]
|
||||
Hostname(std::io::Error),
|
||||
#[error("Error communicating with etcd server: {0}")]
|
||||
Etcd(#[from] etcd_client::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum ConnectError {
|
||||
#[error("Invalid etcd client configuration: {0}")]
|
||||
InvalidConfig(etcd_client::Error),
|
||||
#[error("Failed to load client certificate/key: {0}")]
|
||||
Certificate(std::io::Error),
|
||||
#[error("Failed to load CA certificate: {0}")]
|
||||
CaCert(std::io::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum HandleError {
|
||||
#[error("Error communicating with etcd: {0}")]
|
||||
Etcd(#[from] etcd_client::Error),
|
||||
#[error("Invalid UTF-8 etcd key")]
|
||||
Utf8(#[from] std::str::Utf8Error),
|
||||
#[error("Could not parse YAML: {0}")]
|
||||
Yaml(#[from] serde_yml::Error),
|
||||
#[error("Error decoding content: {0}")]
|
||||
Content(#[from] ContentError),
|
||||
}
|
||||
|
||||
pub(crate) struct Luci {
|
||||
client: Option<Client>,
|
||||
config: Configuration,
|
||||
tx: Sender,
|
||||
processor: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl Luci {
|
||||
pub fn new(config: Configuration) -> Self {
|
||||
let path_prefix = config.path_prefix.clone();
|
||||
let (tx, processor) = Processor::new(path_prefix).start();
|
||||
Self {
|
||||
client: None,
|
||||
config,
|
||||
tx,
|
||||
processor,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect(&mut self) -> Result<&Client, ConnectError> {
|
||||
if let Some(client) = self.client.as_mut() {
|
||||
if let Err(e) = client.status().await {
|
||||
error!("Lost connection to etcd server: {}", e);
|
||||
self.client.take();
|
||||
} else {
|
||||
return Ok(self.client.as_ref().unwrap());
|
||||
}
|
||||
}
|
||||
let addrs = &self.config.etcd.hosts;
|
||||
let mut backoff = Backoff::default();
|
||||
let options = self.connect_options()?;
|
||||
loop {
|
||||
let mut client = Client::connect(addrs, options.clone())
|
||||
.await
|
||||
.map_err(ConnectError::InvalidConfig)?;
|
||||
match client.status().await {
|
||||
Ok(s) => {
|
||||
info!("Connected to etcd server version {}", s.version())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to connect to etcd: {:?}", e);
|
||||
backoff.sleep().await;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
self.client = Some(client);
|
||||
return Ok(self.client.as_ref().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stop(self) {
|
||||
debug!("Notifying processor to stop");
|
||||
drop(self.tx);
|
||||
if let Err(e) = self.processor.await {
|
||||
error!("Error in processor task: {}", e);
|
||||
}
|
||||
debug!("Processor stopped");
|
||||
}
|
||||
|
||||
pub async fn watch(&mut self) -> Result<(), WatchError> {
|
||||
let key = format!(
|
||||
"{}/{}/config",
|
||||
self.config.namespace,
|
||||
hostname::get()
|
||||
.map_err(WatchError::Hostname)?
|
||||
.to_string_lossy()
|
||||
);
|
||||
let get_options = GetOptions::new().with_prefix();
|
||||
let watch_options = WatchOptions::new().with_prefix();
|
||||
if let Some(client) = self.client.as_mut() {
|
||||
let mut watch_client = client.watch_client();
|
||||
debug!("Fetching initial data from key: {}", key);
|
||||
notify_init();
|
||||
let response = client.get(&key[..], Some(get_options)).await?;
|
||||
for kv in response.kvs() {
|
||||
if let Err(e) = self.handle_kv(kv).await {
|
||||
error!("Error handling response: {}", e);
|
||||
}
|
||||
}
|
||||
debug!("Setting up watch for key: {}", key);
|
||||
let (_, mut stream) =
|
||||
watch_client.watch(&key[..], Some(watch_options)).await?;
|
||||
info!("Watching keys under '{}'", key);
|
||||
notify_ready();
|
||||
while let Some(message) = stream.message().await? {
|
||||
trace!("Received message: {:?}", message);
|
||||
for event in message.events() {
|
||||
if let Some(kv) = event.kv() {
|
||||
if let Err(e) = self.handle_kv(kv).await {
|
||||
error!("Error handling message: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error!("Lost connection to watch stream");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn connect_options(&self) -> Result<Option<ConnectOptions>, ConnectError> {
|
||||
if self.config.etcd.client_cert.is_none()
|
||||
&& self.config.etcd.ca_cert.is_none()
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
let mut tls_options = TlsOptions::new();
|
||||
if let Some(c) = self.config.etcd.client_cert.as_ref() {
|
||||
let key = if let Some(k) = self.config.etcd.client_key.as_ref() {
|
||||
std::fs::read(k).map_err(ConnectError::Certificate)?
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let cert = std::fs::read(c).map_err(ConnectError::Certificate)?;
|
||||
tls_options = tls_options.identity(Identity::from_pem(cert, key));
|
||||
}
|
||||
if let Some(c) = self.config.etcd.ca_cert.as_ref() {
|
||||
tls_options = tls_options.ca_certificate(Certificate::from_pem(
|
||||
std::fs::read(c).map_err(ConnectError::CaCert)?,
|
||||
));
|
||||
}
|
||||
Ok(Some(ConnectOptions::new().with_tls(tls_options)))
|
||||
}
|
||||
|
||||
async fn handle_kv(&mut self, kv: &KeyValue) -> Result<(), HandleError> {
|
||||
let key = String::from_utf8_lossy(kv.key());
|
||||
debug!("Handling notification for key {}", key);
|
||||
let value = serde_yml::from_slice(kv.value())?;
|
||||
if self.tx.send((key.into(), value)).await.is_err() {
|
||||
error!("Failed to send entry to processor");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_ready() {
|
||||
let state = &[
|
||||
NotifyState::Status("Watching for etcd key changes"),
|
||||
NotifyState::Ready,
|
||||
];
|
||||
if let Err(e) = sd_notify::notify(true, state) {
|
||||
warn!("Failed to send ready notification to service manager: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_init() {
|
||||
let state = &[NotifyState::Status("Fetching initial configuration")];
|
||||
if let Err(e) = sd_notify::notify(false, state) {
|
||||
warn!("Failed to send status notification to service manager: {}", e);
|
||||
}
|
||||
}
|
115
src/main.rs
115
src/main.rs
|
@ -1,89 +1,74 @@
|
|||
mod backoff;
|
||||
mod config;
|
||||
mod luci;
|
||||
mod model;
|
||||
mod processor;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use etcd_client::Client;
|
||||
use tokio::sync::Notify;
|
||||
use tokio::signal::unix::SignalKind;
|
||||
use tracing::{debug, info, error, trace};
|
||||
use tokio::sync::Notify;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use config::Configuration;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt()
|
||||
.without_time()
|
||||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||
.with_writer(std::io::stderr)
|
||||
.init();
|
||||
|
||||
let stop = Arc::new(Notify::new());
|
||||
let my_stop = stop.clone();
|
||||
let task = tokio::spawn(async move {
|
||||
run(&stop).await
|
||||
});
|
||||
if let Err(e) = wait_signal().await {
|
||||
error!("Failed to set up signal handler: {}", e);
|
||||
std::process::exit(1);
|
||||
let mut args = std::env::args_os();
|
||||
let config_path = args.nth(1).map(PathBuf::from);
|
||||
|
||||
let config = match Configuration::load(config_path.as_deref()) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to load configuration: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
my_stop.notify_waiters();
|
||||
if let Err(e) = task.await {
|
||||
error!("{}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let stop = Arc::new(Notify::new());
|
||||
let stopper = stop.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = wait_signal().await {
|
||||
error!("Failed to set up signal handler: {}", e);
|
||||
}
|
||||
stopper.notify_waiters();
|
||||
});
|
||||
run(config, &stop).await;
|
||||
}
|
||||
|
||||
async fn run(stop: &Notify) {
|
||||
async fn run(config: Configuration, stop: &Notify) {
|
||||
let mut luci = luci::Luci::new(config);
|
||||
loop {
|
||||
let mut client = tokio::select! {
|
||||
c = connect() => c,
|
||||
_ = stop.notified() => return
|
||||
};
|
||||
let (_, mut stream) = match client.watch("dustin", None).await {
|
||||
Ok((w, s)) => (w, s),
|
||||
Err(e) => {
|
||||
error!("Error setting up watch: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
info!("Watch stream established");
|
||||
tokio::select! {
|
||||
m = stream.message() => {
|
||||
trace!("{:?}", m);
|
||||
}
|
||||
r = luci.connect() => if let Err(e) = r {
|
||||
error!("Unable to connect to etcd server: {}", e);
|
||||
break;
|
||||
},
|
||||
_ = stop.notified() => break,
|
||||
};
|
||||
tokio::select! {
|
||||
r = luci.watch() => if let Err(e) = r {
|
||||
error!("Error in watch stream: {}", e);
|
||||
},
|
||||
_ = stop.notified() => break,
|
||||
};
|
||||
if tokio::time::timeout(Duration::from_secs(1), stop.notified())
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
info!("Shutting down");
|
||||
}
|
||||
|
||||
async fn connect() -> Client {
|
||||
let mut duration = Duration::from_millis(250);
|
||||
let addrs = [
|
||||
"http://127.0.0.1:2379",
|
||||
];
|
||||
loop {
|
||||
let mut client = match Client::connect(&addrs, None).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to connect to etcd: {}", e);
|
||||
tokio::time::sleep(duration).await;
|
||||
if duration.as_secs() < 300 {
|
||||
duration *= 2;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
// Client::connect() always succeeds?
|
||||
match client.status().await {
|
||||
Ok(s) => info!("Connected: {:?}", s),
|
||||
Err(e) => {
|
||||
error!("Failed to connect to etcd: {}", e);
|
||||
tokio::time::sleep(duration).await;
|
||||
if duration.as_secs() < 300 {
|
||||
duration *= 2;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return client;
|
||||
}
|
||||
luci.stop().await;
|
||||
}
|
||||
|
||||
async fn wait_signal() -> std::io::Result<()> {
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ContentError {
|
||||
#[error("Base64 decoding error: {0}")]
|
||||
Base64(#[from] base64::DecodeError),
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum ContentEncoding {
|
||||
Raw,
|
||||
Base64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct Hooks {
|
||||
#[serde(default)]
|
||||
pub changed: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct ConfigEntry {
|
||||
path: PathBuf,
|
||||
owner: Option<String>,
|
||||
group: Option<String>,
|
||||
mode: Option<String>,
|
||||
#[serde(default)]
|
||||
content_encoding: ContentEncoding,
|
||||
content: String,
|
||||
#[serde(default)]
|
||||
hooks: Hooks,
|
||||
}
|
||||
|
||||
impl Default for ContentEncoding {
|
||||
fn default() -> Self {
|
||||
Self::Raw
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigEntry {
|
||||
pub fn path(&self) -> &Path {
|
||||
self.path.as_ref()
|
||||
}
|
||||
|
||||
pub fn owner(&self) -> Option<&str> {
|
||||
self.owner.as_deref()
|
||||
}
|
||||
|
||||
pub fn group(&self) -> Option<&str> {
|
||||
self.group.as_deref()
|
||||
}
|
||||
|
||||
pub fn mode(&self) -> Option<&str> {
|
||||
self.mode.as_deref()
|
||||
}
|
||||
|
||||
pub fn content(&self) -> Result<Vec<u8>, ContentError> {
|
||||
match &self.content_encoding {
|
||||
ContentEncoding::Raw => Ok(self.content.as_bytes().into()),
|
||||
ContentEncoding::Base64 => Ok(base64_decode(&self.content)?),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hooks(&self) -> &Hooks {
|
||||
&self.hooks
|
||||
}
|
||||
}
|
||||
|
||||
fn base64_decode(value: &str) -> Result<Vec<u8>, base64::DecodeError> {
|
||||
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, value)
|
||||
}
|
|
@ -0,0 +1,286 @@
|
|||
use std::io::{Read, Seek, Write};
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use blake2::{Blake2b512, Digest};
|
||||
use file_mode::{Mode, ModePath};
|
||||
use shlex::Shlex;
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::Instrument;
|
||||
use tracing::{debug, error, info, trace, warn};
|
||||
|
||||
use crate::model::{ConfigEntry, ContentError};
|
||||
|
||||
pub type Message = (String, ConfigEntry);
|
||||
pub type Sender = mpsc::Sender<Message>;
|
||||
pub type Receiver = mpsc::Receiver<Message>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum ProcessError {
|
||||
#[error("Content error: {0}")]
|
||||
Content(#[from] ContentError),
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SetPermissionsError {
|
||||
#[error("{0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("User not found: {0}")]
|
||||
UserNotFound(String),
|
||||
#[error("Group not found: {0}")]
|
||||
GroupNotFound(String),
|
||||
#[error("Bad mode string: {0}")]
|
||||
BadMode(#[from] file_mode::ModeParseError),
|
||||
}
|
||||
|
||||
pub struct Processor {
|
||||
path_prefix: PathBuf,
|
||||
}
|
||||
|
||||
impl Processor {
|
||||
pub fn new(path_prefix: PathBuf) -> Self {
|
||||
Self { path_prefix }
|
||||
}
|
||||
|
||||
pub fn start(self) -> (Sender, JoinHandle<()>) {
|
||||
let (tx, rx) = mpsc::channel(512);
|
||||
let handle = tokio::spawn(async move { self.run(rx).await });
|
||||
(tx, handle)
|
||||
}
|
||||
|
||||
async fn process(&self, entry: ConfigEntry) -> Result<(), ProcessError> {
|
||||
let path = self
|
||||
.path_prefix
|
||||
.join(entry.path().strip_prefix("/").unwrap_or(entry.path()));
|
||||
let changed = write_file(&path, &entry.content()?)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
if entry.owner().is_some() || entry.group().is_some() {
|
||||
if let Err(e) = chown(&path, entry.owner(), entry.group()) {
|
||||
error!(
|
||||
"Failed to set ownership of {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(mode) = entry.mode() {
|
||||
if let Err(e) = chmod(&path, mode) {
|
||||
error!("Failed to set mode of {}: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
info!("File {} was changed", path.display());
|
||||
let hooks = entry.hooks();
|
||||
debug!("Processing hooks: {:?}", hooks);
|
||||
for hook in hooks.changed.iter() {
|
||||
if let Some(args) = parse_hook(hook, &path) {
|
||||
run_hook(args).await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("File {} was NOT changed", path.display());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run(self, mut rx: Receiver) {
|
||||
while let Some((key, entry)) = rx.recv().await {
|
||||
let span = tracing::debug_span!("process", key = key);
|
||||
if let Err(e) = self.process(entry).instrument(span).await {
|
||||
error!("Error while processing config entry: {}", e)
|
||||
}
|
||||
}
|
||||
debug!("Channel closed");
|
||||
}
|
||||
}
|
||||
|
||||
fn checksum(path: impl AsRef<Path>) -> std::io::Result<Vec<u8>> {
|
||||
let mut f = std::fs::File::open(path)?;
|
||||
let mut blake = Blake2b512::new();
|
||||
loop {
|
||||
let mut buf = vec![0u8; 16384];
|
||||
let sz = f.read(&mut buf)?;
|
||||
if sz == 0 {
|
||||
break;
|
||||
}
|
||||
blake.update(&buf[..sz]);
|
||||
}
|
||||
Ok(blake.finalize().to_vec())
|
||||
}
|
||||
|
||||
fn write_file(
|
||||
dest: impl AsRef<Path>,
|
||||
data: &[u8],
|
||||
) -> Result<bool, std::io::Error> {
|
||||
if let Some(p) = dest.as_ref().parent() {
|
||||
if !p.exists() {
|
||||
info!("Creating directory {}", p.display());
|
||||
std::fs::create_dir_all(p)?;
|
||||
}
|
||||
}
|
||||
if let Ok(orig_cksm) = checksum(&dest) {
|
||||
trace!(
|
||||
"Original checksum: {}: {}",
|
||||
dest.as_ref().display(),
|
||||
hex::encode(&orig_cksm)
|
||||
);
|
||||
let mut blake = Blake2b512::new();
|
||||
blake.update(data);
|
||||
let new_cksm = blake.finalize().to_vec();
|
||||
trace!(
|
||||
"New checksum: {}: {}",
|
||||
dest.as_ref().display(),
|
||||
hex::encode(&new_cksm)
|
||||
);
|
||||
if orig_cksm == new_cksm {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
debug!("Writing output: {}", dest.as_ref().display());
|
||||
let mut f = std::fs::File::create(&dest)?;
|
||||
f.write_all(data)?;
|
||||
let size = f.stream_position()?;
|
||||
debug!("Wrote output: {} ({} bytes)", dest.as_ref().display(), size);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn chown(
|
||||
path: impl AsRef<Path>,
|
||||
owner: Option<&str>,
|
||||
group: Option<&str>,
|
||||
) -> Result<(), SetPermissionsError> {
|
||||
let uid = if let Some(owner) = owner {
|
||||
debug!("Looking up UID for user {}", owner);
|
||||
if let Some(pw) = pwd_grp::getpwnam(owner)? {
|
||||
debug!("Found UID {} for user {}", pw.uid, owner);
|
||||
Some(pw.uid)
|
||||
} else {
|
||||
return Err(SetPermissionsError::UserNotFound(owner.into()));
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let gid = if let Some(group) = group {
|
||||
debug!("Looking up GID for group {}", group);
|
||||
if let Some(gr) = pwd_grp::getgrnam(group)? {
|
||||
debug!("Found GID {} for group {}", gr.gid, group);
|
||||
Some(gr.gid)
|
||||
} else {
|
||||
return Err(SetPermissionsError::GroupNotFound(group.into()));
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let metadata = std::fs::metadata(&path)?;
|
||||
if Some(metadata.uid()) != uid || Some(metadata.gid()) != gid {
|
||||
debug!(
|
||||
"Setting ownership of {} to {:?} / {:?}",
|
||||
path.as_ref().display(),
|
||||
uid,
|
||||
gid
|
||||
);
|
||||
std::os::unix::fs::chown(path, uid, gid)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn chmod(
|
||||
path: impl AsRef<Path>,
|
||||
mode: &str,
|
||||
) -> Result<(), SetPermissionsError> {
|
||||
let current_mode = path.as_ref().mode()?;
|
||||
let mut filemode = current_mode.clone();
|
||||
match u32::from_str_radix(mode, 8) {
|
||||
Ok(m) => filemode.set(&Mode::from(m)),
|
||||
Err(_) => filemode.set_str_umask(mode, 0o777)?,
|
||||
}
|
||||
if current_mode != filemode {
|
||||
debug!(
|
||||
"Changing mode of {} from {} to {}",
|
||||
path.as_ref().display(),
|
||||
current_mode,
|
||||
filemode,
|
||||
);
|
||||
let newmode = file_mode::Mode::from(filemode.set_mode_path(&path)?);
|
||||
info!("Set mode of {} to {}", path.as_ref().display(), newmode);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_hook(command: &str, path: &Path) -> Option<Vec<String>> {
|
||||
let mut bad_path = false;
|
||||
let args: Vec<_> = Shlex::new(command)
|
||||
.map(|a| {
|
||||
if a == "%s" {
|
||||
if let Some(p) = path.as_os_str().to_str() {
|
||||
p.into()
|
||||
} else {
|
||||
bad_path = true;
|
||||
a
|
||||
}
|
||||
} else {
|
||||
a
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if bad_path {
|
||||
warn!("Cannot run hook: path is not valid UTF-8");
|
||||
return None;
|
||||
}
|
||||
if args.is_empty() {
|
||||
warn!(
|
||||
"Invalid hook for {} ({}): empty argument list",
|
||||
path.display(),
|
||||
command
|
||||
);
|
||||
return None;
|
||||
}
|
||||
Some(args)
|
||||
}
|
||||
|
||||
async fn run_hook(args: Vec<String>) {
|
||||
match shlex::try_join(args.iter().map(String::as_str)) {
|
||||
Ok(cmd) => info!("Running hook: {}", cmd),
|
||||
Err(e) => {
|
||||
error!("Invalid hook: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Err(e) = _run_hook(args).await {
|
||||
error!("Error running hook: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
async fn _run_hook(args: Vec<String>) -> std::io::Result<()> {
|
||||
let status = Command::new(&args[0])
|
||||
.args(&args[1..])
|
||||
.stdin(std::process::Stdio::null())
|
||||
.spawn()?
|
||||
.wait()
|
||||
.await?;
|
||||
match status.code() {
|
||||
Some(0) => info!("Hook completed successfully"),
|
||||
Some(c) => error!("Hook exited with status {}", c),
|
||||
#[cfg(unix)]
|
||||
None => {
|
||||
if let Some(signal) = status.signal() {
|
||||
error!("Hook killed by signal {}", signal);
|
||||
} else {
|
||||
error!("Hook exited with unknown status");
|
||||
}
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
None => error!("Hook exited with unknown status"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue