diff --git a/.gitignore b/.gitignore index 0aee99a..ec8dea2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .DS_Store .idea/ -/target +**/target *.iml **/*.rs.bk pg_regress/results diff --git a/Cargo.lock b/Cargo.lock index e1feabf..ab536a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -206,9 +212,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", ] @@ -242,7 +248,7 @@ dependencies = [ "pin-project-lite", "rand 0.9.4", "rustls", - "rustls-native-certs", + "rustls-native-certs 0.8.4", "rustls-pemfile", "rustls-pki-types", "serde", @@ -270,7 +276,7 @@ dependencies = [ "prost-types 0.14.4", "tonic 0.14.6", "tonic-prost", - "ureq", + "ureq 3.3.0", ] [[package]] @@ -361,9 +367,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.63" +version = "1.2.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" dependencies = [ "find-msvc-tools", "shlex 2.0.1", @@ -507,6 +513,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -532,6 +548,15 @@ dependencies = [ "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 = "crypto-common" version = "0.2.2" @@ -590,7 +615,6 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", "serde_core", ] @@ -682,7 +706,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -701,18 +725,33 @@ dependencies = [ "tower-service", ] +[[package]] +name = "etcd_client_sync" +version = "0.1.0" +dependencies = [ + "base64", + "serde", + "serde_json", + "thiserror 2.0.18", + "ureq 2.12.1", +] + [[package]] name = "etcd_fdw" version = "0.0.0" dependencies = [ + "base64", "etcd-client", - "futures", + "etcd_client_sync", "pgrx", "pgrx-tests", + "serde", + "serde_json", "supabase-wrappers", "testcontainers", "thiserror 2.0.18", "tokio", + "ureq 2.12.1", ] [[package]] @@ -770,6 +809,16 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -930,9 +979,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "h2" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", @@ -1137,7 +1186,7 @@ dependencies = [ "hyper", "libc", "pin-project-lite", - "socket2 0.6.4", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -1358,9 +1407,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.100" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" dependencies = [ "cfg-if", "futures-util", @@ -1443,9 +1492,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "mime" @@ -1459,6 +1508,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.1" @@ -1598,6 +1657,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -1887,9 +1952,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "postgres" -version = "0.19.13" +version = "0.19.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aacf632d0554ff75f58183694f41dc8999c8a3a43a386994d0ec2d034f1dfbe1" +checksum = "33ad20e0aa0b24f5a394eab4f78c781d248982b22b25cecc7e3aa46a681605bd" dependencies = [ "bytes", "fallible-iterator", @@ -1901,9 +1966,9 @@ dependencies = [ [[package]] name = "postgres-protocol" -version = "0.6.11" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56201207dac53e2f38e848e31b4b91616a6bb6e0c7205b77718994a7f49e70fc" +checksum = "08808e3c483c46e999108051c78334f473d5adb59d78bb80a1268c7e6aa6c514" dependencies = [ "base64", "byteorder", @@ -1919,9 +1984,9 @@ dependencies = [ [[package]] name = "postgres-types" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dc729a129e682e8d24170cd30ae1aa01b336b096cbb56df6d534ffec133d186" +checksum = "851ca9db4932932d69f3ea811b1abe63087a0f740a47692619dd40d4899b68be" dependencies = [ "bytes", "fallible-iterator", @@ -2250,7 +2315,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2268,16 +2333,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe 0.1.6", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework 2.11.1", +] + [[package]] name = "rustls-native-certs" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ - "openssl-probe", + "openssl-probe 0.2.1", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.7.0", ] [[package]] @@ -2387,6 +2465,19 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -2394,7 +2485,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -2570,6 +2661,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + [[package]] name = "siphasher" version = "1.0.3" @@ -2584,9 +2681,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" @@ -2663,7 +2760,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "supabase-wrappers" version = "0.1.26" -source = "git+https://github.com/cybertec-postgresql/wrappers.git?branch=develop#9d0a4b35b430973196dcf52e0aa6302af9186183" +source = "git+https://github.com/cybertec-postgresql/wrappers.git?branch=develop#04294be8e58137a2da6eb01901615a8c00e52487" dependencies = [ "pgrx", "supabase-wrappers-macros", @@ -2675,7 +2772,7 @@ dependencies = [ [[package]] name = "supabase-wrappers-macros" version = "0.1.18" -source = "git+https://github.com/cybertec-postgresql/wrappers.git?branch=develop#9d0a4b35b430973196dcf52e0aa6302af9186183" +source = "git+https://github.com/cybertec-postgresql/wrappers.git?branch=develop#04294be8e58137a2da6eb01901615a8c00e52487" dependencies = [ "proc-macro2", "quote", @@ -2756,10 +2853,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2833,12 +2930,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde_core", @@ -2848,15 +2944,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d" dependencies = [ "num-conv", "time-core", @@ -2917,9 +3013,9 @@ dependencies = [ [[package]] name = "tokio-postgres" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd8df5ef180f6364759a6f00f7aadda4fbbac86cdee37480826a6ff9f3574ce" +checksum = "a528f7d280f6d5b9cd149635c8705b0dd049754bc67d81d31fa25169a93809d3" dependencies = [ "async-trait", "byteorder", @@ -3245,6 +3341,25 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-native-certs 0.7.3", + "rustls-pki-types", + "serde", + "serde_json", + "url", + "webpki-roots 0.26.11", +] + [[package]] name = "ureq" version = "3.3.0" @@ -3353,9 +3468,9 @@ dependencies = [ [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen 0.57.1", ] @@ -3380,9 +3495,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.123" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" dependencies = [ "cfg-if", "once_cell", @@ -3393,9 +3508,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.123" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3403,9 +3518,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.123" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" dependencies = [ "bumpalo", "proc-macro2", @@ -3416,9 +3531,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.123" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" dependencies = [ "unicode-ident", ] @@ -3459,9 +3574,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.100" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0871acf327f283dc6da28a1696cdc64fb355ba9f935d052021fa77f35cce69" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" dependencies = [ "js-sys", "wasm-bindgen", @@ -3477,6 +3592,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.7", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "whoami" version = "2.1.2" @@ -3512,7 +3645,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3880,18 +4013,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.50" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.50" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", @@ -3921,9 +4054,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml index e852a74..d0a9c04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,22 +21,30 @@ pg18 = ["pgrx/pg18", "pgrx-tests/pg18", "supabase-wrappers/pg18"] # `cargo pgrx test` builds the lib with `--features pg_test` (no cfg(test)), so # the integration-test module compiled in by this feature needs testcontainers # as a normal dependency. Gating it here keeps it out of release builds. -pg_test = ["dep:testcontainers"] +pg_test = ["dep:testcontainers", "dep:etcd-client", "dep:tokio"] [dependencies] -etcd-client = { version = "0.16", features = ["tls"] } -futures = "0.3.31" -pgrx = {version="=0.16.1"} -supabase-wrappers = { git="https://github.com/cybertec-postgresql/wrappers.git", branch="develop", default-features = false } +base64 = "0.22" +etcd_client_sync = { path = "etcd_client" } +pgrx = { version = "=0.16.1" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +supabase-wrappers = { git = "https://github.com/cybertec-postgresql/wrappers.git", branch = "develop", default-features = false } thiserror = "2.0.16" -tokio = { version = "1.47.1", features = ["full"] } +ureq = { version = "2.9", features = ["tls", "native-certs", "json"] } # Test-only and not built for release: enabled only by the `pg_test` feature # (for `cargo pgrx test`) and via dev-dependencies (for `cargo test`). -testcontainers = { version = "0.25.0", features = ["blocking"], optional = true } +testcontainers = { version = "0.25.0", features = [ + "blocking", +], optional = true } +etcd-client = { version = "0.16", features = ["tls"], optional = true } +tokio = { version = "1.47.1", features = ["full"], optional = true } [dev-dependencies] +etcd-client = { version = "0.16", features = ["tls"] } pgrx-tests = "=0.16.1" testcontainers = { version = "0.25.0", features = ["blocking"] } +tokio = { version = "1.47.1", features = ["full"] } [profile.dev] panic = "unwind" diff --git a/etcd_client/Cargo.lock b/etcd_client/Cargo.lock new file mode 100644 index 0000000..9b78968 --- /dev/null +++ b/etcd_client/Cargo.lock @@ -0,0 +1,921 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcd_client_sync" +version = "0.1.0" +dependencies = [ + "base64", + "serde", + "serde_json", + "thiserror", + "tokio", + "ureq", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "serde", + "serde_json", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.7", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/etcd_client/Cargo.toml b/etcd_client/Cargo.toml new file mode 100644 index 0000000..11e18c8 --- /dev/null +++ b/etcd_client/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "etcd_client_sync" +version = "0.1.0" +edition = "2021" + +[dependencies] +base64 = "0.22" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "2.0" +ureq = { version = "2.9", features = ["tls", "native-certs", "json"] } + +[dev-dependencies] +tokio = { version = "1.47", features = ["full"] } diff --git a/etcd_client/src/lib.rs b/etcd_client/src/lib.rs new file mode 100644 index 0000000..d253b61 --- /dev/null +++ b/etcd_client/src/lib.rs @@ -0,0 +1,340 @@ +//! Synchronous etcd v3 HTTP client using ureq + +use base64::Engine; +use serde::{Deserialize, Serialize}; +use std::time::Duration; +use thiserror::Error; + +pub use base64::engine::general_purpose::STANDARD as BASE64; + +#[derive(Error, Debug)] +pub enum EtcdError { + #[error("HTTP error: {0}")] + Http(String), + #[error("JSON error: {0}")] + Json(String), + #[error("etcd error: {0}")] + Etcd(String), +} + +/// Options for range queries +#[derive(Default, Clone)] +pub struct RangeOptions { + pub range_end: Option, + pub limit: Option, + pub revision: Option, + pub serializable: bool, + pub keys_only: bool, + pub sort_order: Option, + pub sort_target: Option, + pub prefix: bool, +} + +impl RangeOptions { + pub fn with_range(mut self, range_end: String) -> Self { + self.range_end = Some(range_end); + self + } + pub fn with_limit(mut self, limit: u64) -> Self { + self.limit = Some(limit); + self + } + pub fn with_revision(mut self, revision: i64) -> Self { + self.revision = Some(revision); + self + } + pub fn with_serializable(mut self) -> Self { + self.serializable = true; + self + } + pub fn with_keys_only(mut self) -> Self { + self.keys_only = true; + self + } + pub fn with_sort(mut self, target: i32, order: i32) -> Self { + self.sort_target = Some(target); + self.sort_order = Some(order); + self + } + pub fn with_prefix(mut self) -> Self { + self.prefix = true; + self + } +} + +/// etcd v3 API key-value pair +#[derive(Clone, Debug)] +pub struct EtcdKeyValue { + key: Vec, + value: Vec, +} + +impl EtcdKeyValue { + pub fn key(&self) -> &[u8] { + &self.key + } + pub fn value(&self) -> &[u8] { + &self.value + } +} + +/// Synchronous etcd HTTP client using ureq +pub struct EtcdHttpClient { + endpoint: String, + timeout: Duration, + username: Option, + password: Option, + token: Option, +} + +impl EtcdHttpClient { + pub fn new(endpoint: String, timeout: Duration) -> Self { + // Add http:// prefix if no scheme is present + let endpoint = if endpoint.starts_with("http://") || endpoint.starts_with("https://") { + endpoint + } else { + format!("http://{}", endpoint) + }; + Self { + endpoint, + timeout, + username: None, + password: None, + token: None, + } + } + + pub fn with_auth(mut self, username: String, password: String) -> Self { + self.username = Some(username); + self.password = Some(password); + self + } + + fn base64_encode(input: &str) -> String { + BASE64.encode(input.as_bytes()) + } + + fn base64_decode(input: &str) -> Result, base64::DecodeError> { + BASE64.decode(input) + } + + /// Authenticate with etcd and get a token + pub fn authenticate(&mut self) -> Result<(), EtcdError> { + let (Some(user), Some(pass)) = (&self.username, &self.password) else { + return Ok(()); // No auth configured + }; + + #[derive(Serialize)] + struct AuthRequest { + name: String, + password: String, + } + + #[derive(Deserialize)] + struct AuthResponse { + token: String, + } + + let req = AuthRequest { + name: user.clone(), + password: pass.clone(), + }; + + let url = format!("{}/v3/auth/authenticate", self.endpoint); + let resp = ureq::Agent::new() + .post(&url) + .timeout(self.timeout) + .send_json(&req) + .map_err(|e| EtcdError::Http(e.to_string()))?; + + let auth_resp: AuthResponse = resp + .into_json() + .map_err(|e| EtcdError::Json(e.to_string()))?; + + self.token = Some(auth_resp.token); + Ok(()) + } + + pub fn range( + &mut self, + key: &str, + options: RangeOptions, + ) -> Result, EtcdError> { + // Ensure we have a token if auth is required + if self.token.is_none() && (self.username.is_some() || self.password.is_some()) { + self.authenticate()?; + } + + #[derive(Serialize)] + struct RangeRequest { + key: String, + #[serde(skip_serializing_if = "Option::is_none")] + range_end: Option, + #[serde(skip_serializing_if = "Option::is_none")] + limit: Option, + #[serde(skip_serializing_if = "Option::is_none")] + revision: Option, + #[serde(skip_serializing_if = "Option::is_none")] + serializable: Option, + #[serde(skip_serializing_if = "Option::is_none")] + keys_only: Option, + #[serde(skip_serializing_if = "Option::is_none")] + count_only: Option, + #[serde(skip_serializing_if = "Option::is_none")] + sort_order: Option, + #[serde(skip_serializing_if = "Option::is_none")] + sort_target: Option, + } + + #[derive(Deserialize)] + struct RangeResponse { + kvs: Option>, + count: Option, + } + + #[derive(Deserialize)] + struct KvPair { + key: String, + value: String, + } + + let req = RangeRequest { + key: Self::base64_encode(key), + range_end: options.range_end.map(|s| Self::base64_encode(&s)), + limit: options.limit, + revision: options.revision, + serializable: if options.serializable { + Some(true) + } else { + None + }, + keys_only: if options.keys_only { Some(true) } else { None }, + count_only: None, + sort_order: options.sort_order, + sort_target: options.sort_target, + }; + + let url = format!("{}/v3/kv/range", self.endpoint); + let mut req_builder = ureq::Agent::new().post(&url).timeout(self.timeout); + if let Some(t) = &self.token { + req_builder = req_builder.set("Authorization", t); + } + let resp = req_builder + .send_json(&req) + .map_err(|e| EtcdError::Http(e.to_string()))?; + + let range_resp: RangeResponse = resp + .into_json() + .map_err(|e| EtcdError::Json(e.to_string()))?; + + let kvs = range_resp + .kvs + .unwrap_or_default() + .into_iter() + .map(|kv| EtcdKeyValue { + key: Self::base64_decode(&kv.key).unwrap_or_default(), + value: Self::base64_decode(&kv.value).unwrap_or_default(), + }) + .collect(); + + Ok(kvs) + } + + pub fn put(&mut self, key: &str, value: &str) -> Result<(), EtcdError> { + // Ensure we have a token if auth is required + if self.token.is_none() && (self.username.is_some() || self.password.is_some()) { + self.authenticate()?; + } + + #[derive(Serialize)] + struct PutRequest { + key: String, + value: String, + } + + let req = PutRequest { + key: Self::base64_encode(key), + value: Self::base64_encode(value), + }; + + let url = format!("{}/v3/kv/put", self.endpoint); + let mut req_builder = ureq::Agent::new().post(&url).timeout(self.timeout); + if let Some(t) = &self.token { + req_builder = req_builder.set("Authorization", t); + } + req_builder + .send_json(&req) + .map_err(|e| EtcdError::Http(e.to_string()))?; + + Ok(()) + } + + pub fn delete(&mut self, key: &str) -> Result { + // Ensure we have a token if auth is required + if self.token.is_none() && (self.username.is_some() || self.password.is_some()) { + self.authenticate()?; + } + + #[derive(Serialize)] + struct DeleteRequest { + key: String, + } + + #[derive(Deserialize)] + struct DeleteResponse { + deleted: serde_json::Value, + } + + let req = DeleteRequest { + key: Self::base64_encode(key), + }; + + let url = format!("{}/v3/kv/deleterange", self.endpoint); + let mut req_builder = ureq::Agent::new().post(&url).timeout(self.timeout); + if let Some(t) = &self.token { + req_builder = req_builder.set("Authorization", t); + } + let resp = req_builder + .send_json(&req) + .map_err(|e| EtcdError::Http(e.to_string()))?; + + let del_resp: DeleteResponse = resp + .into_json() + .map_err(|e| EtcdError::Json(e.to_string()))?; + + // Handle both string and number formats + let deleted = match del_resp.deleted { + serde_json::Value::Number(n) => n.as_u64().unwrap_or(0), + serde_json::Value::String(s) => s.parse().unwrap_or(0), + _ => 0, + }; + + Ok(deleted) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_base64_encode_decode() { + let original = "hello world"; + let encoded = EtcdHttpClient::base64_encode(original); + assert_eq!(encoded, "aGVsbG8gd29ybGQ="); + let decoded = EtcdHttpClient::base64_decode(&encoded).unwrap(); + assert_eq!(String::from_utf8(decoded).unwrap(), original); + } + + #[test] + fn test_range_options_builder() { + let opts = RangeOptions::default() + .with_prefix() + .with_limit(10) + .with_revision(5); + + assert!(opts.prefix); + assert_eq!(opts.limit, Some(10)); + assert_eq!(opts.revision, Some(5)); + } +} diff --git a/src/lib.rs b/src/lib.rs index bdd8772..9656d0e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,12 @@ -use etcd_client::{Client, ConnectOptions, TlsOptions, Identity, Certificate, Error, DeleteOptions, GetOptions, KeyValue, PutOptions, SortTarget, SortOrder}; -use std::time::Duration; use pgrx::pg_sys::panic::ErrorReport; use pgrx::PgSqlErrorCode; use pgrx::*; +use std::time::Duration; use supabase_wrappers::prelude::*; use thiserror::Error; +use etcd_client_sync::{EtcdHttpClient, EtcdKeyValue, RangeOptions}; + pgrx::pg_module_magic!(); #[wrappers_fdw( @@ -14,13 +15,8 @@ pgrx::pg_module_magic!(); error_type = "EtcdFdwError" )] pub(crate) struct EtcdFdw { - // `client` is declared before `rt` so that it is dropped first: the etcd - // client's background work can wind down while its runtime is still alive. - // The connection is established lazily (see `ensure_connected`). - client: Option, - rt: Runtime, config: EtcdConfig, - fetch_results: Vec, + fetch_results: Vec, fetch_pos: usize, tgt_cols: Vec, } @@ -105,6 +101,12 @@ pub enum EtcdFdwError { OptionsError(#[from] OptionsError), } +impl From for EtcdFdwError { + fn from(value: etcd_client_sync::EtcdError) -> Self { + EtcdFdwError::FetchError(value.to_string()) + } +} + impl From for ErrorReport { fn from(value: EtcdFdwError) -> Self { ErrorReport::new(PgSqlErrorCode::ERRCODE_FDW_ERROR, format!("{}", value), "") @@ -113,11 +115,7 @@ impl From for ErrorReport { /// Check whether dependent options exits /// i.e user & pass, cert & key -fn require_pair( - a: bool, - b: bool, - err: EtcdFdwError, -) -> Result<(), EtcdFdwError> { +fn require_pair(a: bool, b: bool, err: EtcdFdwError) -> Result<(), EtcdFdwError> { match (a, b) { (true, false) | (false, true) => Err(err), _ => Ok(()), @@ -140,54 +138,19 @@ fn parse_timeout( } } - - -/// Use this to connect to etcd. -/// Parse the certs/key paths and read them as bytes -/// Sets the `TlsOptions` if available to support sll connection -pub async fn connect_etcd(config: EtcdConfig) -> Result { - let mut connect_options = ConnectOptions::new() - .with_connect_timeout(config.connect_timeout) - .with_timeout(config.request_timeout); - - let use_tls = config.ca_cert_path.is_some() || config.client_cert_path.is_some(); - - if use_tls { - let mut tls_options = TlsOptions::new(); - - // Load CA cert if provided - if let Some(ca_path) = &config.ca_cert_path { - let ca_bytes = std::fs::read(ca_path).map_err(Error::IoError)?; - let ca_cert = Certificate::from_pem(ca_bytes); - tls_options = tls_options.ca_certificate(ca_cert); - } - - // Load client cert and key if both provided - if let (Some(cert_path), Some(key_path)) = (&config.client_cert_path, &config.client_key_path) { - let cert_bytes = std::fs::read(cert_path).map_err(Error::IoError)?; - let key_bytes = std::fs::read(key_path).map_err(Error::IoError)?; - let identity = Identity::from_pem(cert_bytes, key_bytes); - tls_options = tls_options.identity(identity); - } - - // Load domain name if provided - if let Some(domain) = &config.servername { - tls_options = tls_options.domain_name(domain); - } - - connect_options = connect_options.with_tls(tls_options); - } - - // Load User and Password - if let (Some(user), Some(pass)) = (&config.user, &config.password) { - connect_options = connect_options.with_user(user, pass); - } - - let endpoints: Vec<&str> = config.endpoints.iter().map(|s| s.as_str()).collect(); - Client::connect(endpoints, Some(connect_options)).await +/// Create a synchronous etcd HTTP client +fn connect_etcd(config: EtcdConfig) -> EtcdFdwResult { + // Use the first endpoint as the base URL for HTTP requests + let endpoint = config.endpoints.first().cloned().unwrap_or_default(); + let client = EtcdHttpClient::new(endpoint, config.request_timeout); + let client = if let (Some(user), Some(pass)) = (config.user, config.password) { + client.with_auth(user, pass) + } else { + client + }; + Ok(client) } - type EtcdFdwResult = std::result::Result; /// Read a text column's value out of a `Cell` directly, instead of going @@ -217,22 +180,6 @@ fn required_cell(row: &Row, col: &str) -> EtcdFdwResult { } } -impl EtcdFdw { - /// Establish the etcd connection on first use. Connecting lazily keeps - /// `new()` cheap (it can run at planning time) and avoids opening a - /// connection for statements that are planned but never executed. - fn ensure_connected(&mut self) -> EtcdFdwResult<()> { - if self.client.is_none() { - let client = self - .rt - .block_on(connect_etcd(self.config.clone())) - .map_err(|e| EtcdFdwError::ClientConnectionError(e.to_string()))?; - self.client = Some(client); - } - Ok(()) - } -} - impl ForeignDataWrapper for EtcdFdw { fn new(server: ForeignServer) -> EtcdFdwResult { // Connection string for the etcd server (required). @@ -316,22 +263,10 @@ impl ForeignDataWrapper for EtcdFdw { request_timeout, }; - // Use the framework's current-thread runtime helper. A Postgres backend - // is single-threaded and the FDW only ever `block_on`s, so there is no - // reason to spawn worker threads (one per CPU core, as the multi-threaded - // `Runtime::new()` does). Avoiding a worker pool means no background - // threads that could receive Postgres signals off the main thread, and - // none that could be leaked if a Postgres error unwinds through this - // struct. `create_async_runtime()` is exactly - // `Builder::new_current_thread().enable_all().build()`. - let rt = create_async_runtime().map_err(|e| EtcdFdwError::RuntimeInitError(e.to_string()))?; - // The etcd connection is established lazily in `begin_scan` / // `begin_modify`: `new()` must stay cheap because the framework can call // it at planning time, possibly for a statement that is never executed. Ok(Self { - client: None, - rt, config, fetch_results: vec![], fetch_pos: 0, @@ -351,28 +286,37 @@ impl ForeignDataWrapper for EtcdFdw { let prefix = options.get("prefix").cloned(); let range_end = options.get("range_end").cloned(); let key_start = options.get("key").cloned(); - let keys_only = options.get("keys_only").map(|v| v == "true").unwrap_or(false); - let revision = options.get("revision").and_then(|v| v.parse::().ok()).unwrap_or(0); - let serializable = options.get("consistency").map(|v| v == "s").unwrap_or(false); + let keys_only = options + .get("keys_only") + .map(|v| v == "true") + .unwrap_or(false); + let revision = options + .get("revision") + .and_then(|v| v.parse::().ok()) + .unwrap_or(0); + let serializable = options + .get("consistency") + .map(|v| v == "s") + .unwrap_or(false); let mut qual_key_start: Option = None; let mut qual_prefix: Option = None; let mut qual_range_end: Option = None; - let mut get_options = GetOptions::new(); + let mut range_options = RangeOptions::default(); if let Some(x) = limit { - get_options = get_options.with_limit(x.count); + range_options = range_options.with_limit(x.count as u64); } if keys_only { - get_options = get_options.with_keys_only(); + range_options = range_options.with_keys_only(); } if revision > 0 { - get_options = get_options.with_revision(revision); + range_options = range_options.with_revision(revision); } if serializable { - get_options = get_options.with_serializable(); + range_options = range_options.with_serializable(); } // WHERE clause pushdown @@ -503,43 +447,36 @@ impl ForeignDataWrapper for EtcdFdw { // Otherwise, use the effective key start let key = match &eff_prefix { Some(p) => { - get_options = get_options.with_prefix(); + range_options = range_options.with_prefix(); // Ensure the key starts from the larger of the prefix or the effective key start std::cmp::max(eff_key_start.clone(), p.clone()) } None => eff_key_start.clone(), }; - get_options = get_options.with_range(eff_range_end); + range_options = range_options.with_range(eff_range_end); // sort pushdown + // etcd v3 API: sort_target 0=KEY, 1=VALUE; sort_order 0=ASCEND, 1=DESCEND if let Some(first_sort) = sort.first() { let field_name = first_sort.field.to_ascii_uppercase(); - if let Some(target) = SortTarget::from_str_name(&field_name) { - let order = if first_sort.reversed { - SortOrder::Descend - } else { - SortOrder::Ascend - }; + let sort_target = match field_name.as_str() { + "KEY" => 0, + "VALUE" => 1, + _ => return Err(EtcdFdwError::InvalidSortField(first_sort.field.clone())), + }; + let sort_order = if first_sort.reversed { 1 } else { 0 }; - get_options = get_options.with_sort(target, order); - } else { - return Err(EtcdFdwError::InvalidSortField(first_sort.field.clone())); - } + range_options = range_options.with_sort(sort_target, sort_order); } - self.ensure_connected()?; - let client = self - .client - .as_mut() - .expect("client must be connected after ensure_connected"); - let result = self.rt.block_on(client.get(key, Some(get_options))); - let mut result_unwrapped = match result { + let mut client = connect_etcd(self.config.clone())?; + let result = client.range(&key, range_options); + self.fetch_results = match result { Ok(x) => x, Err(e) => return Err(EtcdFdwError::FetchError(e.to_string())), }; - self.fetch_results = result_unwrapped.take_kvs(); self.fetch_pos = 0; self.tgt_cols = columns.to_vec(); Ok(()) @@ -591,29 +528,20 @@ impl ForeignDataWrapper for EtcdFdw { let key = key_string.as_str(); let value = value_string.as_str(); - self.ensure_connected()?; - let client = self - .client - .as_mut() - .expect("client must be connected after ensure_connected"); + let mut client = connect_etcd(self.config.clone())?; // Reject duplicates: error if the key already exists. - match self.rt.block_on(client.get(key, None)) { - Ok(x) => { - if x.kvs().iter().any(|kv| kv.key() == key.as_bytes()) { + match client.range(key, RangeOptions::default()) { + Ok(kvs) => { + if kvs.iter().any(|kv| kv.key() == key.as_bytes()) { return Err(EtcdFdwError::KeyAlreadyExists(key.to_string())); } } Err(e) => return Err(EtcdFdwError::FetchError(e.to_string())), } - match self - .rt - .block_on(client.put(key, value, Some(PutOptions::new()))) - { - Ok(_) => Ok(()), - Err(e) => Err(EtcdFdwError::UpdateError(e.to_string())), - } + client.put(key, value)?; + Ok(()) } fn update(&mut self, rowid: &Cell, new_row: &Row) -> Result<(), EtcdFdwError> { @@ -623,62 +551,45 @@ impl ForeignDataWrapper for EtcdFdw { let value_string = required_cell(new_row, "value")?; let value = value_string.as_str(); - self.ensure_connected()?; - let client = self - .client - .as_mut() - .expect("client must be connected after ensure_connected"); + let mut client = connect_etcd(self.config.clone())?; // The key must already exist; an exact `get` returns it or nothing. - match self.rt.block_on(client.get(key, None)) { - Ok(x) => { - if !x.kvs().iter().any(|kv| kv.key() == key.as_bytes()) { + match client.range(key, RangeOptions::default()) { + Ok(kvs) => { + if !kvs.iter().any(|kv| kv.key() == key.as_bytes()) { return Err(EtcdFdwError::KeyDoesntExist(key.to_string())); } } Err(e) => return Err(EtcdFdwError::FetchError(e.to_string())), } - match self.rt.block_on(client.put(key, value, None)) { - Ok(_) => Ok(()), - Err(e) => Err(EtcdFdwError::UpdateError(e.to_string())), - } + client.put(key, value)?; + Ok(()) } fn delete(&mut self, rowid: &Cell) -> Result<(), EtcdFdwError> { let key_string = cell_to_text(rowid); let key = key_string.as_str(); - self.ensure_connected()?; - let client = self - .client - .as_mut() - .expect("client must be connected after ensure_connected"); + let mut client = connect_etcd(self.config.clone())?; - match self.rt.block_on(client.get(key, None)) { - Ok(x) => { - if !x.kvs().iter().any(|kv| kv.key() == key.as_bytes()) { + match client.range(key, RangeOptions::default()) { + Ok(kvs) => { + if !kvs.iter().any(|kv| kv.key() == key.as_bytes()) { return Err(EtcdFdwError::KeyDoesntExist(key.to_string())); } } Err(e) => return Err(EtcdFdwError::FetchError(e.to_string())), } - match self - .rt - .block_on(client.delete(key, Some(DeleteOptions::new()))) - { - Ok(x) => { - if x.deleted() == 0 { - return Err(EtcdFdwError::UpdateError(format!( - "Deletion seemingly successful, but deleted count is {}", - x.deleted() - ))); - } - Ok(()) - } - Err(e) => Err(EtcdFdwError::UpdateError(e.to_string())), + let deleted = client.delete(key)?; + if deleted == 0 { + return Err(EtcdFdwError::UpdateError(format!( + "Deletion seemingly successful, but deleted count is {}", + deleted + ))); } + Ok(()) } // fn get_rel_size( @@ -705,7 +616,11 @@ impl ForeignDataWrapper for EtcdFdw { let cacert_path_exists = check_options_contain(&options, "ssl_ca").is_ok(); let cert_path_exists = check_options_contain(&options, "ssl_cert").is_ok(); - require_pair(cacert_path_exists, cert_path_exists, EtcdFdwError::CertKeyMismatch(()))?; + require_pair( + cacert_path_exists, + cert_path_exists, + EtcdFdwError::CertKeyMismatch(()), + )?; } else if oid == FOREIGN_TABLE_RELATION_ID { check_options_contain(&options, "rowid_column")?; @@ -724,7 +639,11 @@ impl ForeignDataWrapper for EtcdFdw { let user_exists = check_options_contain(&options, "user").is_ok(); let password_exists = check_options_contain(&options, "password").is_ok(); - require_pair(user_exists, password_exists, EtcdFdwError::UserPassMismatch(()))?; + require_pair( + user_exists, + password_exists, + EtcdFdwError::UserPassMismatch(()), + )?; } } @@ -751,7 +670,7 @@ mod tests { use std::time::Duration; use super::*; - use etcd_client::Permission; + use etcd_client::{Client, ConnectOptions, Permission}; use testcontainers::{ core::{IntoContainerPort, WaitFor}, runners::SyncRunner, @@ -777,11 +696,13 @@ mod tests { // add root user and role client.role_add("root").await.expect("add role"); - client.user_add(ETCD_USER, ETCD_PASS, None) + client + .user_add(ETCD_USER, ETCD_PASS, None) .await .expect("add user"); - client.user_grant_role(ETCD_USER, "root") + client + .user_grant_role(ETCD_USER, "root") .await .expect("grant role"); @@ -808,7 +729,7 @@ mod tests { .get_host_port_ipv4(2379.tcp()) .expect("Exposed host port should be available"); - let url = format!("{}:{}", host, port); + let url = format!("http://{}:{}", host, port); let rt = tokio::runtime::Runtime::new().expect("Tokio runtime should be initialized"); rt.block_on(etcd_auth_setup(url.clone())); (container, url) @@ -927,7 +848,10 @@ mod tests { let query_result = Spi::get_two::("SELECT * FROM test WHERE key = 'key3'") .expect("SELECT * with key filter should work"); - assert_eq!((Some(format!("key3")), Some(format!("value3"))), query_result); + assert_eq!( + (Some(format!("key3")), Some(format!("value3"))), + query_result + ); } #[pg_test] @@ -976,39 +900,52 @@ mod tests { Spi::run("SELECT * FROM test;").expect("SELECT should work"); }); - assert!(result.is_err(), "Expected SELECT to fail due to invalid user mapping"); + assert!( + result.is_err(), + "Expected SELECT to fail due to invalid user mapping" + ); // Setup: create a role and user with limited permissions in etcd let rt = tokio::runtime::Runtime::new().expect("Tokio runtime should be initialized"); - rt.block_on( - async { - let mut client: Client = Client::connect([url.clone()], Some(ConnectOptions::new().with_user(ETCD_USER, ETCD_PASS))) - .await - .expect("connect etcd"); - client.role_add("rw_role").await.expect("add role"); - // role with read and write permissions on keys starting with "/" - client.role_grant_permission("rw_role", Permission::with_from_key(Permission::read_write("/"))) - .await - .expect("grant permission"); - client.user_add("etcd_user", "secret", None) - .await - .expect("add user"); - client.user_grant_role("etcd_user", "rw_role") - .await - .expect("grant role"); - } - ); + rt.block_on(async { + let mut client: Client = Client::connect( + [url.clone()], + Some(ConnectOptions::new().with_user(ETCD_USER, ETCD_PASS)), + ) + .await + .expect("connect etcd"); + client.role_add("rw_role").await.expect("add role"); + // role with read and write permissions on keys starting with "/" + client + .role_grant_permission( + "rw_role", + Permission::with_from_key(Permission::read_write("/")), + ) + .await + .expect("grant permission"); + client + .user_add("etcd_user", "secret", None) + .await + .expect("add user"); + client + .user_grant_role("etcd_user", "rw_role") + .await + .expect("grant role"); + }); // Alter user mapping to use the new limited permissions user Spi::run("ALTER USER MAPPING FOR CURRENT_USER SERVER etcd_test_server OPTIONS (SET user 'etcd_user', SET password 'secret');") .expect("Alter user mapping should work"); // Test 2: Selecting a key outside of the user's permissions (should fail) - let invalid_result = std::panic::catch_unwind(|| { + let invalid_result = std::panic::catch_unwind(|| { Spi::run("SELECT * FROM test;").expect("SELECT should work"); }); - assert!(invalid_result.is_err(), "Expected SELECT to fail due to insufficient permissions"); + assert!( + invalid_result.is_err(), + "Expected SELECT to fail due to insufficient permissions" + ); // Test 3: Selecting a key within the user's permissions let result = Spi::get_two::("SELECT * FROM test WHERE key = '/gather'") @@ -1054,6 +991,40 @@ mod tests { ); } + // Regression test for the backend crash (SIGSEGV) that occurred when a + // full-table scan was executed twice in succession. The global `ureq::Agent` + // connection pool retained a stale socket after the first scan; reusing it + // on the second scan crashed the backend. The fix creates a fresh agent per + // HTTP call, so two consecutive scans must both succeed. + #[pg_test] + fn test_repeated_full_scan_does_not_crash() { + let (_container, url) = create_container(); + create_fdt(url); + + Spi::run("INSERT INTO test (key, value) VALUES ('k1','v1'),('k2','v2'),('k3','v3')") + .expect("INSERT should work"); + + // Mimics the user's reproduce_etcd_fdw_crash(): a plpgsql FOR loop over + // SELECT * FROM test, called twice. The first call primes any internal + // state; the second call previously triggered the crash. + Spi::run( + "DO $$ DECLARE r record; BEGIN FOR r IN SELECT * FROM test LOOP NULL; END LOOP; END $$;", + ) + .expect("First full-scan loop should succeed"); + + let result = std::panic::catch_unwind(|| { + Spi::run( + "DO $$ DECLARE r record; BEGIN FOR r IN SELECT * FROM test LOOP NULL; END LOOP; END $$;", + ) + .expect("Second full-scan loop should succeed"); + }); + + assert!( + result.is_ok(), + "Second full-scan loop crashed the backend (regression)" + ); + } + // Regression test for the data-corruption bug where insert/update/delete ran // the cell through Display (which renders a String as `'foo'`) and then // stripped surrounding single quotes, mangling any key/value that legitimately