diff --git a/rust/.gitignore b/rust/.gitignore new file mode 100644 index 00000000..9f3ad1f5 --- /dev/null +++ b/rust/.gitignore @@ -0,0 +1,2 @@ +target +.adminapirc diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 00000000..fc1190c8 --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,2188 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "adminapi" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "env_logger", + "futures", + "hmac", + "log", + "reqwest", + "serde", + "serde_json", + "sha1", + "signature", + "ssh-agent-client-rs", + "ssh-encoding", + "ssh-key", + "tokio", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[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 = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" + +[[package]] +name = "cc" +version = "1.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b9470d453346108f93a59222a9a1a5724db32d0a4727b7ab7ace4b4d822dc9" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[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 = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dsa" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48bc224a9084ad760195584ce5abb3c2c34a225fa312a128ad245a6b412b7689" +dependencies = [ + "digest", + "num-bigint-dig", + "num-traits", + "pkcs8", + "rfc6979", + "sha2", + "signature", + "zeroize", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "sha2", + "subtle", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "fnv" +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" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core", + "sha2", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" +dependencies = [ + "bytes", + "rand", + "ring", + "rustc-hash", + "rustls", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e346e016eacfff12233c243718197ca12f148c84e1e84268a896699b41c71780" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "windows-registry", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "sha2", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[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.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "ssh-agent-client-rs" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc9798a2e1390157853f81735b9815be2a45dd2200c36646aad942de9074d78" +dependencies = [ + "bytes", + "signature", + "ssh-encoding", + "ssh-key", + "thiserror", +] + +[[package]] +name = "ssh-cipher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" +dependencies = [ + "cipher", + "ssh-encoding", +] + +[[package]] +name = "ssh-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" +dependencies = [ + "base64ct", + "pem-rfc7468", + "sha2", +] + +[[package]] +name = "ssh-key" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3" +dependencies = [ + "dsa", + "ed25519-dalek", + "num-bigint-dig", + "p256", + "p384", + "p521", + "rand_core", + "rsa", + "sec1", + "serde", + "sha1", + "sha2", + "signature", + "ssh-cipher", + "ssh-encoding", + "subtle", + "zeroize", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[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.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[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.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +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-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets", +] + +[[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.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[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 = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 00000000..fbf8adc5 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "adminapi" +version = "0.1.0" +edition = "2021" +description = "The ServerAdmin API available in Rust" +license = "MIT" +readme = "README.md" +authors = ["InnoGames System Administration "] +repository = "https://github.com/innogames/serveradmin/" + +[dependencies] +anyhow = "1.0" +hmac = "0.12" +log = "0.4" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sha1 = "0.10" +signature = "2.2" +ssh-agent-client-rs = "0.9" +ssh-encoding = { version = "0.2", features = ["base64"] } +ssh-key = { version = "0.6", features = ["serde", "ed25519", "dsa", "crypto", "rsa"] } + +[dev-dependencies] +env_logger = "0.11" +tokio = { version = "1.38", features = ["full"] } +clap = "4.5" +futures = "0.3" + +[profile.release] +lto = true +opt-level = "z" +strip = true diff --git a/rust/README.md b/rust/README.md new file mode 100644 index 00000000..ca76e261 --- /dev/null +++ b/rust/README.md @@ -0,0 +1,21 @@ +# Adminapi in Rust! + +This is the client library for ServerAdmin in Rust! + +## ServerAdmin + +Serveradmin is the central server database management system of InnoGames. +It has an HTTP web interface and an HTTP JSON API. Check out `the documentation +`_ or watch `this FOSDEM 19 +talk `_ for a deepdive how +InnoGames works with serveradmin. + +## Features of this library + +| Feature | Status | Note | +|--------------|-----------------|----------------------------------------------------------------------------------------------------------------------------------------| +| Querying | Implemented | Querying works by either a generic `serde_json::Value`, or with a `serde` supported type. | +| Creating | Implemented | Creating/Changing/Deleting is implemented via `adminapi::commit::Commit` object. (may change later) | +| *other* APIs | Not implemented | The adminapi supports other APIs such as the "firewall" api. The support for those is currently not implemented | +| Token Auth | Implemented | | +| SSH Auth | Implemented | The implementation either uses the SSH agent using the `SSH_AUTH_SOCK` env variable or reads a private key from `SERVERADMIN_KEY_PATH` | diff --git a/rust/examples/adminapi.rs b/rust/examples/adminapi.rs new file mode 100644 index 00000000..26e80aec --- /dev/null +++ b/rust/examples/adminapi.rs @@ -0,0 +1,24 @@ +use adminapi::cli::parse_filter_args; +use adminapi::query::Query; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + env_logger::init(); + + let mut args = std::env::args(); + args.next(); + + let filters = parse_filter_args(args)?; + + let query = Query::builder() + .filters(filters) + .restrict(["hostname", "responsible_admin", "os"]) + .build(); + + let servers = query.request().await?; + for server in servers.into_iter() { + println!("{server:#?}"); + } + + Ok(()) +} diff --git a/rust/examples/commit.rs b/rust/examples/commit.rs new file mode 100644 index 00000000..12019557 --- /dev/null +++ b/rust/examples/commit.rs @@ -0,0 +1,41 @@ +use adminapi::filter::{empty, not, regexp}; +use adminapi::new_object::NewObject; +use adminapi::query::Query; + +#[tokio::main] +pub async fn main() -> anyhow::Result<()> { + env_logger::init(); + let mut server = Query::builder() + .filter("hostname", regexp(".*payment-staging.*")) + .filter("os", not(empty())) + .restrict(["hostname", "responsible_admin", "os"]) + .build() + .request() + .await? + .all() + .pop() + .ok_or(anyhow::anyhow!("No servers returned"))?; + + server + .set("os", "bookworm")? + .add("responsible_admin", "yannik.schwiegerr")? + .remove("responsible_admin", "yannik.schwieger")?; + + server.commit().await?; + + let mut new_sg = NewObject::request_new("service_group").await?; + new_sg + .set("hostname", "yannik-adminapi-rs-2.test.sg")? + .set("project", "test")? + .add("responsible_admin", "yannik.schwieger")? + .add("protocol_ports_inbound", "tcp443")?; + let mut created_sg = new_sg.commit().await?; + created_sg + .add("protocol_ports_inbound", "tcp80")? + .add("sg_allow_from", "yannik-adminapi-rs-2.test.sg")? + .add("sg_allow_to", "yannik-adminapi-rs-2.test.sg")? + .commit() + .await?; + + Ok(()) +} diff --git a/rust/examples/parse_args.rs b/rust/examples/parse_args.rs new file mode 100644 index 00000000..2cf80a51 --- /dev/null +++ b/rust/examples/parse_args.rs @@ -0,0 +1,15 @@ +use adminapi::cli::parse_filter_args; + +pub fn main() { + let mut args = std::env::args(); + args.next(); + + match parse_filter_args(args) { + Ok(arg) => { + println!("{arg:#?}"); + } + Err(err) => { + eprintln!("{err:#?}"); + } + } +} diff --git a/rust/examples/prepare_os_upgrade.rs b/rust/examples/prepare_os_upgrade.rs new file mode 100644 index 00000000..d9ac5613 --- /dev/null +++ b/rust/examples/prepare_os_upgrade.rs @@ -0,0 +1,99 @@ +use adminapi::cli::parse_filter_args; +use adminapi::commit::AttributeValue; +use adminapi::query::Query; + +#[tokio::main] +pub async fn main() -> anyhow::Result<()> { + env_logger::init(); + + let clap = clap::Command::new("prepare_os_upgrade") + .author("Yannik, yannik.schwieger@innogames.com") + .arg(clap::arg!([OS_RELEASE] "Sets the OS release name").required(true)) + .arg( + clap::arg!([QUERY]) + .action(clap::ArgAction::Append) + .help("Query for the preparation") + .value_parser(clap::value_parser!(String)) + .num_args(1..) + .required(true), + ) + .arg( + clap::arg!(--"puppet-environment" "Sets an optional puppet environment") + .value_parser(clap::value_parser!(String)) + .required(false), + ) + .arg( + clap::arg!(--"maintenance" "Sets the servers to maintenance") + .action(clap::ArgAction::SetTrue) + .required(false), + ); + + let matches = clap.get_matches(); + let os_release = matches + .get_one::("OS_RELEASE") + .expect("OS_RELEASE is required!"); + let query = matches + .get_many::("QUERY") + .unwrap() + .cloned() + .collect::>(); + + if !["buster", "bullseye", "bookworm", "trixie"].contains(&os_release.as_str()) { + return Err(anyhow::anyhow!( + "Error: {os_release} is not a valid Debian release!" + )); + } + + let filters = parse_filter_args(query.into_iter())?; + + let query = Query::builder() + .filters(filters) + .restrict(["os", "repositories", "puppet_environment", "state"]) + .build(); + let response = query.request().await?; + let mut updates = vec![]; + + for mut server in response.all() { + let AttributeValue::String(base_os) = server.get("os") else { + return Err(anyhow::anyhow!("Unexpected value for os")); + }; + + let AttributeValue::Array(repos) = server.get("repositories") else { + return Err(anyhow::anyhow!("Unexpected value for repositories")); + }; + + server.set("os", os_release.to_string())?; + if matches.get_flag("maintenance") { + server.set("state", "maintenance")?; + } + + for repo in repos { + let AttributeValue::String(repo) = repo else { + return Err(anyhow::anyhow!("Unexpected value for repository")); + }; + + if !repo.contains(&base_os) { + continue; + } + + server.add("repositories", repo.replace(&base_os, os_release))?; + server.remove("repositories", repo)?; + + if let Some(environment) = matches.get_one::("puppet-environment") { + if !server.get("puppet_environment").is_null() { + return Err(anyhow::anyhow!( + "Puppet environment is already set. Aborting!" + )); + } + + server.set("puppet_environment", environment.clone())?; + } + } + + updates.push(async move { server.commit().await }); + } + + futures::future::try_join_all(updates).await?; + + Ok(()) +} diff --git a/rust/src/api.rs b/rust/src/api.rs new file mode 100644 index 00000000..07723586 --- /dev/null +++ b/rust/src/api.rs @@ -0,0 +1,315 @@ +use std::fmt::Display; + +use signature::Signer; +use ssh_encoding::Encode; + +use crate::commit::{ + AttributeChange, AttributeValue, Changeset, Commit, Dataset, IntoAttributeValue, +}; +use crate::config::Config; +use crate::query::Query; + +pub const QUERY_ENDPOINT: &str = "/api/dataset/query"; +pub const COMMIT_ENDPOINT: &str = "/api/dataset/commit"; +pub const NEW_OBJECT_ENDPOINT: &str = "/api/dataset/new_object"; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct QueryResponse { + pub status: String, + pub result: Vec>, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct NewObjectResponse { + pub result: Dataset, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct CommitResponse { + pub status: String, + #[serde(default)] + pub message: Option, +} + +pub async fn query_objects( + query: &Query, +) -> anyhow::Result> { + let config = Config::build_from_environment()?; + let response = request_api(QUERY_ENDPOINT, serde_json::to_value(query)?, config).await?; + let response = response.error_for_status()?; + + Ok(response.json().await?) +} + +pub async fn new_object(servertype: impl Display) -> anyhow::Result { + let config = Config::build_from_environment()?; + let response = request_api( + format!("{NEW_OBJECT_ENDPOINT}?servertype={servertype}"), + serde_json::Value::Null, + config, + ) + .await?; + let response = response.error_for_status()?; + + Ok(response.json().await?) +} + +pub async fn commit_changes(commit: &Commit) -> anyhow::Result { + let config = Config::build_from_environment()?; + let response = request_api(COMMIT_ENDPOINT, serde_json::to_value(commit)?, config).await?; + let status = response.status(); + let body = response.json::().await?; + + if status.is_client_error() || status.is_server_error() { + return Err(anyhow::anyhow!("Unable to process request").context(format!("{:?}", body))); + } + + if body.status == "error" { + return Err(anyhow::anyhow!( + "Error while committing {}", + body.message + .unwrap_or_else(|| String::from("Unknown commit error")) + )); + } + + Ok(body) +} + +pub async fn request_api( + endpoint: impl Display, + data: serde_json::Value, + config: Config, +) -> anyhow::Result { + let client = reqwest::Client::new(); + let token = config.auth_token.unwrap_or_default(); + let body = serde_json::to_string(&data)?; + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_secs(); + let sign_body = format!("{now}:{body}"); + let url = format!("{}{endpoint}", config.base_url); + let mut request = client + .post(url) + .header("Content-Type", "application/json") + .header("X-Timestamp", now.to_string()) + .header("X-API-Version", config.api_version) + .body(body.into_bytes()); + + if let Some(signer) = &config.ssh_signer { + let signature = signer.try_sign(sign_body.as_bytes())?; + let len = Encode::encoded_len_prefixed(&signature)?; + let base64_len = (((len.saturating_mul(4)) / 3).saturating_add(3)) & !3; + let mut buf = vec![0; base64_len]; + let mut writer = ssh_encoding::Base64Writer::new(&mut buf)?; + signature.encode(&mut writer)?; + let signature = writer.finish()?; + + request = request + .header("X-PublicKeys", signer.get_public_key()) + .header("X-Signatures", signature.to_string()); + } else { + request = request + .header( + "X-SecurityToken", + calculate_security_token(&token, &sign_body), + ) + .header("X-Application", calculate_app_id(&token)) + } + + Ok(request.send().await?) +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct Server { + pub object_id: u64, + #[serde(flatten)] + pub attributes: T, + #[serde(skip, default)] + pub(crate) changes: Changeset, +} + +impl IntoIterator for QueryResponse { + type Item = Server; + type IntoIter = std::vec::IntoIter>; + + fn into_iter(self) -> Self::IntoIter { + self.result.into_iter() + } +} + +fn calculate_security_token(token: &String, sign_body: &str) -> String { + use hmac::Mac; + + type HmacSha1 = hmac::Hmac; + let mut hmac = + HmacSha1::new_from_slice(token.as_bytes()).expect("Hmac can accept any size of key"); + hmac.update(sign_body.as_bytes()); + let result = hmac.finalize(); + + result + .into_bytes() + .iter() + .fold(String::new(), |hash, byte| format!("{hash}{byte:02x}")) +} + +fn calculate_app_id(token: &String) -> String { + use sha1::Digest; + let mut hasher = sha1::Sha1::new(); + hasher.update(token.as_bytes()); + let result = hasher.finalize(); + + result + .iter() + .fold(String::new(), |hash, byte| format!("{hash}{byte:02x}")) +} + +impl Server { + pub fn clear(&mut self, attribute: impl ToString) -> anyhow::Result<&mut Self> { + self.attributes.clear(attribute.to_string()); + + Ok(self) + } + + pub fn set( + &mut self, + attribute: impl ToString, + value: impl IntoAttributeValue + 'static, + ) -> anyhow::Result<&mut Self> { + let new = value.into_attribute_value(); + let attribute = attribute.to_string(); + + if self.attributes.get(&attribute).is_array() { + return Err(anyhow::anyhow!( + "Attribute {attribute} is a multi attribute, set is not supported!" + )); + } + + let old = self.attributes.get(&attribute); + self.attributes.set(attribute.clone(), new.clone()); + self.changes + .attributes + .insert(attribute, AttributeChange::Update { old, new }); + + Ok(self) + } + + pub fn add( + &mut self, + attribute: impl ToString, + value: impl IntoAttributeValue + 'static, + ) -> anyhow::Result<&mut Self> { + let value = value.into_attribute_value(); + let attribute = attribute.to_string(); + + if !self.attributes.get(&attribute).is_array() { + return Err(anyhow::anyhow!( + "add is only supported with multi attributes" + )); + } + + if self + .attributes + .get(&attribute) + .as_array() + .unwrap() + .contains(&value) + { + return Ok(self); + } + + self.attributes.add(attribute.clone(), value.clone()); + let entry = self + .changes + .attributes + .entry(attribute) + .or_insert(AttributeChange::Multi { + remove: vec![], + add: vec![], + }); + + if let AttributeChange::Multi { add, .. } = entry { + add.push(value); + } + + Ok(self) + } + + pub fn remove( + &mut self, + attribute: impl ToString, + value: impl IntoAttributeValue + 'static, + ) -> anyhow::Result<&mut Self> { + let value = value.into_attribute_value(); + let attribute = attribute.to_string(); + + if !self.attributes.get(&attribute).is_array() { + return Err(anyhow::anyhow!( + "remove is only supported with multi attributes" + )); + } + + if !self + .attributes + .get(&attribute) + .as_array() + .unwrap() + .contains(&value) + { + return Ok(self); + } + + self.attributes.remove(attribute.clone(), value.clone()); + let entry = self + .changes + .attributes + .entry(attribute) + .or_insert(AttributeChange::Multi { + remove: vec![], + add: vec![], + }); + + if let AttributeChange::Multi { remove, .. } = entry { + remove.push(value); + } + + Ok(self) + } + + pub fn get(&self, name: &str) -> AttributeValue { + self.attributes.get(name) + } + + pub fn changeset(&self) -> Changeset { + let mut set = self.changes.clone(); + set.object_id = self.object_id; + + set + } + + /// Rolls back the changes. + /// + /// Returns the reverted changes + pub fn rollback(&mut self) -> Changeset { + let old_changes = self.changeset(); + + self.changes = Changeset::default(); + + old_changes + } +} + +impl QueryResponse { + pub fn one(mut self) -> anyhow::Result> { + if self.result.len() > 1 { + return Err(anyhow::anyhow!("Result has more then one item!")); + } + + self.result + .pop() + .ok_or(anyhow::anyhow!("No result returned!")) + } + + pub fn all(self) -> Vec> { + self.result + } +} diff --git a/rust/src/cli.rs b/rust/src/cli.rs new file mode 100644 index 00000000..ab6e7c0f --- /dev/null +++ b/rust/src/cli.rs @@ -0,0 +1,112 @@ +use std::str::Chars; + +use crate::filter; +use crate::filter::{AttributeFilter, FilterValue, IntoFilterValue}; + +pub fn parse_filter_args( + args: impl Iterator + 'static, +) -> anyhow::Result { + let mut filter = AttributeFilter::default(); + + for arg in args { + let mut split = arg.split('='); + let Some(attribute) = split.next() else { + continue; + }; + let tail = split.collect::(); + if tail.is_empty() { + return Err(anyhow::anyhow!("Attribute value is missing")); + } + + filter.insert(attribute.to_string(), parse_filter_arg(tail)); + } + + Ok(filter) +} + +pub fn parse_filter_arg(arg: String) -> FilterValue { + let mut chars = arg.chars(); + + lookup_function(&mut chars) +} + +pub fn lookup_function(chars: &mut Chars) -> FilterValue { + let mut buffer = String::new(); + let mut fn_name = String::new(); + let mut depth = 0; + let mut inner = Vec::new(); + + for char in chars.by_ref() { + if char == '(' { + depth += 1; + } + if char == ')' { + depth -= 1; + } + + if char == '(' && depth == 1 { + fn_name.extend(buffer.drain(0..)); + } + + if char == '(' && depth == 1 { + continue; + } + + if char == ')' && depth == 0 { + continue; + } + + if depth == 0 && char == ' ' { + inner.push(buffer.clone()); + buffer.clear(); + + continue; + } + + buffer.push(char); + } + + if fn_name.is_empty() && inner.is_empty() { + return buffer.into_filter_value(); + } + + if !buffer.is_empty() { + inner.push(buffer); + } + + let filter_fn = get_filter_function(&fn_name.to_lowercase()); + let mut inner_filters = inner + .into_iter() + .map(|filter| { + let mut chars = filter.chars(); + + lookup_function(&mut chars) + }) + .collect::>(); + + if inner_filters.len() == 1 { + filter_fn(inner_filters.pop().unwrap()) + } else { + filter_fn(FilterValue::Array(inner_filters)) + } +} + +fn get_filter_function(name: &str) -> Box FilterValue> { + match name { + "all" => Box::new(filter::all), + "any" => Box::new(filter::any), + "containedby" => Box::new(filter::contained_by), + "containedonlyby" => Box::new(filter::contained_only_by), + "contains" => Box::new(filter::contains), + "empty" => Box::new(|_| filter::empty()), + "greaterthan" => Box::new(filter::greater_than), + "greaterthanorequals" => Box::new(filter::greater_than_or_equals), + "lessthan" => Box::new(filter::less_than), + "lessthanorequals" => Box::new(filter::less_than_or_equals), + "not" => Box::new(filter::not), + "overlaps" => Box::new(filter::overlaps), + "regexp" => Box::new(filter::regexp), + "startswith" => Box::new(filter::starts_with), + _name => Box::new(IntoFilterValue::into_filter_value), + } +} diff --git a/rust/src/commit.rs b/rust/src/commit.rs new file mode 100644 index 00000000..0ee0e22a --- /dev/null +++ b/rust/src/commit.rs @@ -0,0 +1,237 @@ +use std::collections::{HashMap, HashSet}; + +use serde_json::Number; + +pub type AttributeValue = serde_json::Value; + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct Dataset(HashMap); + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(tag = "action", rename_all = "snake_case")] +pub enum AttributeChange { + Update { + old: AttributeValue, + new: AttributeValue, + }, + Multi { + remove: Vec, + add: Vec, + }, +} + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct Changeset { + pub object_id: u64, + #[serde(flatten)] + pub attributes: HashMap, +} + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct Commit { + #[serde(skip_serializing_if = "Vec::is_empty")] + pub created: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub changed: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub deleted: Vec, +} + +pub trait IntoDataset { + fn into_dataset(self) -> Dataset; +} + +pub trait IntoAttributeValue { + fn into_attribute_value(self) -> AttributeValue; +} + +impl Changeset { + pub fn has_changes(&self) -> bool { + self.attributes + .iter() + .filter(|(_, change)| { + if let AttributeChange::Multi { add, remove } = change { + return add.len() + remove.len() > 0; + } + + if let AttributeChange::Update { new, old } = change { + return new.ne(old); + } + + true + }) + .any(|_| true) + } +} + +impl Commit { + pub fn new() -> Self { + Self { + created: Default::default(), + changed: Default::default(), + deleted: Default::default(), + } + } + + pub fn create(mut self, attrs: impl IntoDataset + 'static) -> Self { + self.created.push(attrs.into_dataset()); + + self + } + + pub fn update(mut self, mut changeset: Changeset) -> Self { + let filtered = changeset + .attributes + .into_iter() + .filter(|(_, change)| { + if let AttributeChange::Update { new, old } = change { + return new.ne(old); + } + + if let AttributeChange::Multi { add, remove } = change { + return add.len() + remove.len() > 0; + } + + true + }) + .collect(); + + changeset.attributes = filtered; + + self.changed.push(changeset); + + self + } + + pub fn delete(mut self, object_id: u64) -> Self { + self.deleted.push(object_id); + + self + } +} + +impl IntoDataset for Dataset { + fn into_dataset(self) -> Dataset { + self + } +} + +impl IntoAttributeValue for String { + fn into_attribute_value(self) -> AttributeValue { + AttributeValue::String(self) + } +} + +impl IntoAttributeValue for &String { + fn into_attribute_value(self) -> AttributeValue { + AttributeValue::String(self.clone()) + } +} + +impl IntoAttributeValue for () { + fn into_attribute_value(self) -> AttributeValue { + AttributeValue::Null + } +} + +impl IntoAttributeValue for f32 { + fn into_attribute_value(self) -> AttributeValue { + AttributeValue::from(self) + } +} + +impl IntoAttributeValue for i32 { + fn into_attribute_value(self) -> AttributeValue { + AttributeValue::Number(Number::from(self)) + } +} + +impl IntoAttributeValue for Vec { + fn into_attribute_value(self) -> AttributeValue { + AttributeValue::from_iter( + self.into_iter() + .map(IntoAttributeValue::into_attribute_value), + ) + } +} + +impl IntoAttributeValue for AttributeValue { + fn into_attribute_value(self) -> AttributeValue { + self + } +} + +impl IntoAttributeValue for &str { + fn into_attribute_value(self) -> AttributeValue { + AttributeValue::String(self.to_string()) + } +} + +impl Dataset { + pub fn new() -> Self { + Self(Default::default()) + } + + pub fn clear(&mut self, name: impl ToString) -> &mut Self { + self.0.remove(&name.to_string()); + + self + } + + pub fn set( + &mut self, + name: impl ToString, + attr: impl IntoAttributeValue + 'static, + ) -> &mut Self { + self.0.insert(name.to_string(), attr.into_attribute_value()); + + self + } + + pub fn add( + &mut self, + name: impl ToString, + attr: impl IntoAttributeValue + 'static, + ) -> &mut Self { + let name = name.to_string(); + let attr = attr.into_attribute_value(); + + let value = self.0.entry(name).or_insert(AttributeValue::Array(vec![])); + if let serde_json::Value::Array(array) = value { + array.push(attr); + } + + self + } + + pub fn remove( + &mut self, + name: impl ToString, + attr: impl IntoAttributeValue + 'static, + ) -> &mut Self { + let name = name.to_string(); + let attr = attr.into_attribute_value(); + + let Some(AttributeValue::Array(value)) = self.0.get_mut(&name) else { + return self; + }; + + if let Some(index) = value.iter().position(|val| val == &attr) { + value.remove(index); + } + + if value.is_empty() { + self.0.remove(&name); + } + + self + } + + pub fn get(&self, name: &str) -> AttributeValue { + self.0.get(name).cloned().unwrap_or(AttributeValue::Null) + } + + pub fn keys(&self) -> HashSet { + self.0.keys().cloned().collect() + } +} diff --git a/rust/src/config.rs b/rust/src/config.rs new file mode 100644 index 00000000..02542a90 --- /dev/null +++ b/rust/src/config.rs @@ -0,0 +1,124 @@ +use std::fmt::{Debug, Formatter}; +use std::path::Path; +use std::sync::{Arc, Mutex}; + +use signature::Signer; + +use crate::API_VERSION; + +pub const ENV_NAME_BASE_URL: &str = "SERVERADMIN_BASE_URL"; +pub const ENV_NAME_TOKEN: &str = "SERVERADMIN_TOKEN"; +pub const ENV_NAME_KEY_PATH: &str = "SERVERADMIN_KEY_PATH"; +pub const ENV_NAME_SSH_AGENT: &str = "SSH_AUTH_SOCK"; + +#[derive(Clone, Debug, Default)] +pub struct Config { + pub base_url: String, + pub api_version: String, + pub ssh_signer: Option, + pub auth_token: Option, +} + +#[derive(Clone)] +pub enum SshSigner { + Agent( + Box, + Box>>, + ), + Key(Box), +} + +impl Config { + pub fn build_from_environment() -> anyhow::Result { + let config = Self { + base_url: std::env::var(ENV_NAME_BASE_URL)? + .trim_end_matches('/') + .trim_end_matches("/api") + .to_string(), + api_version: API_VERSION.to_string(), + ssh_signer: Self::get_signing_key()?, + auth_token: std::env::var(ENV_NAME_TOKEN).ok(), + }; + + Ok(config) + } + + fn get_signing_key() -> anyhow::Result> { + if let Ok(path) = std::env::var(ENV_NAME_KEY_PATH) { + let key = ssh_key::PrivateKey::read_openssh_file(Path::new(&path))?; + + return Ok(Some(SshSigner::Key(Box::new(key)))); + } + + let path = std::env::var(ENV_NAME_SSH_AGENT).unwrap_or_default(); + let client = ssh_agent_client_rs::Client::connect(Path::new(&path)) + .map_err(|error| log::error!("Unable to connect to SSH agent: {error}")) + .ok(); + + if let Some(mut client) = client { + let identities = client.list_identities()?; + for key in identities { + log::debug!( + "Test signing with SSH key {}", + key.fingerprint(ssh_key::HashAlg::Sha256) + ); + + if client.sign(&key, b"mest message").is_ok() { + log::debug!( + "Found compatible key: {}", + key.fingerprint(ssh_key::HashAlg::Sha256) + ); + + return Ok(Some(SshSigner::Agent( + Box::new(key), + Box::new(Arc::new(Mutex::new(client))), + ))); + } + } + } + + Ok(None) + } +} + +impl Debug for SshSigner { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + SshSigner::Key(_key) => f.write_fmt(format_args!("Key: {}", self.get_public_key())), + SshSigner::Agent(_key, _) => { + f.write_fmt(format_args!("Agent: {}", self.get_public_key())) + } + } + } +} + +impl SshSigner { + pub fn get_public_key(&self) -> String { + let public_key = match self { + SshSigner::Key(key) => key.public_key(), + SshSigner::Agent(key, _) => key, + }; + + let key = public_key.to_openssh().unwrap(); + let mut key = key.split(' '); + key.next(); + + key.next().map(ToString::to_string).unwrap_or_default() + } +} + +impl Signer for SshSigner { + fn try_sign(&self, msg: &[u8]) -> Result { + match self { + SshSigner::Key(key) => key.try_sign(msg), + SshSigner::Agent(key, agent) => agent + .lock() + .unwrap() + .sign(key, msg) + .map_err(signature::Error::from_source), + } + } +} + +unsafe impl Send for SshSigner {} +unsafe impl Sync for SshSigner {} diff --git a/rust/src/filter.rs b/rust/src/filter.rs new file mode 100644 index 00000000..a37d3df6 --- /dev/null +++ b/rust/src/filter.rs @@ -0,0 +1,123 @@ +use std::collections::HashMap; + +pub type FilterValue = serde_json::Value; + +pub type AttributeFilter = HashMap; + +pub trait IntoFilterValue { + fn into_filter_value(self) -> FilterValue; +} + +/// ServerAdmin All Filter +pub fn all(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("All", value) +} + +/// ServerAdmin Any Filter +pub fn any(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("Any", value) +} + +/// ServerAdmin ContainedBy Filter +pub fn contained_by(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("ContainedBy", value) +} + +/// ServerAdmin ContainedOnlyBy Filter +pub fn contained_only_by(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("ContainedOnlyBy", value) +} + +/// ServerAdmin Contains Filter +pub fn contains(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("Contains", value) +} + +/// ServerAdmin Empty Filter +pub fn empty() -> FilterValue { + create_filter("Empty", ()) +} + +/// ServerAdmin GreaterThan Filter +pub fn greater_than(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("GreaterThan", value) +} + +/// ServerAdmin GreaterThanOrEquals Filter +pub fn greater_than_or_equals(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("GreaterThanOrEquals", value) +} + +/// ServerAdmin LessThan Filter +pub fn less_than(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("LessThan", value) +} + +/// ServerAdmin LessThanOrEquals Filter +pub fn less_than_or_equals(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("LessThanOrEquals", value) +} + +/// ServerAdmin Not Filter +pub fn not(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("Not", value) +} + +/// ServerAdmin Overlaps Filter +pub fn overlaps(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("Overlaps", value) +} + +/// ServerAdmin Regexp Filter +pub fn regexp(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("Regexp", value) +} + +/// ServerAdmin StartsWith Filter +pub fn starts_with(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("StartsWith", value) +} + +impl IntoFilterValue for () { + fn into_filter_value(self) -> FilterValue { + FilterValue::Null + } +} + +impl IntoFilterValue for String { + fn into_filter_value(self) -> FilterValue { + FilterValue::String(self) + } +} + +impl IntoFilterValue for &str { + fn into_filter_value(self) -> FilterValue { + FilterValue::String(self.to_string()) + } +} + +impl IntoFilterValue for i32 { + fn into_filter_value(self) -> FilterValue { + FilterValue::from(self) + } +} + +impl IntoFilterValue for Vec { + fn into_filter_value(self) -> FilterValue { + FilterValue::from_iter(self.into_iter().map(IntoFilterValue::into_filter_value)) + } +} + +impl IntoFilterValue for serde_json::Value { + fn into_filter_value(self) -> FilterValue { + self + } +} + +/// Filters on an attribute +fn create_filter(filter_name: impl ToString, value: impl IntoFilterValue + 'static) -> FilterValue { + let mut filter = HashMap::new(); + filter.insert(filter_name.to_string(), value.into_filter_value()); + + FilterValue::from_iter(filter) +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 00000000..78fdcd8a --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,9 @@ +pub const API_VERSION: &str = "4.9.0"; + +pub mod api; +pub mod cli; +pub mod commit; +pub mod config; +pub mod filter; +pub mod new_object; +pub mod query; diff --git a/rust/src/new_object.rs b/rust/src/new_object.rs new file mode 100644 index 00000000..f5449b0c --- /dev/null +++ b/rust/src/new_object.rs @@ -0,0 +1,151 @@ +use std::ops::{Deref, DerefMut}; + +use crate::api::{commit_changes, new_object, NewObjectResponse, Server}; +use crate::commit::{AttributeValue, Changeset, Commit, Dataset}; +use crate::query::Query; + +#[derive(Clone, Debug)] +pub struct NewObject { + object_id: Option, + server: Server, + deferred_changes: Changeset, +} + +impl NewObject { + pub fn from_dataset(dataset: Dataset) -> Self { + Self { + object_id: None, + server: Server { + object_id: 0, + attributes: dataset, + changes: Default::default(), + }, + deferred_changes: Default::default(), + } + } + + pub async fn request_new(servertype: impl ToString) -> anyhow::Result { + let servertype = servertype.to_string(); + let NewObjectResponse { result } = new_object(&servertype).await?; + + Ok(Self { + object_id: None, + server: Server { + object_id: 0, + attributes: result, + changes: Default::default(), + }, + deferred_changes: Default::default(), + }) + } + + pub async fn get_or_create( + servertype: impl ToString, + hostname: impl ToString, + ) -> anyhow::Result { + let mut new_object = Self::request_new(servertype.to_string()).await?; + + if let Ok(server) = Query::builder() + .filter("hostname", hostname.to_string()) + .restrict(new_object.server.attributes.keys()) + .build() + .request() + .await? + .one() + { + new_object.object_id = Some(server.object_id); + new_object.server = server; + } + + Ok(new_object) + } + + pub fn is_new(&self) -> bool { + self.object_id.is_none() + } + + pub fn has_changes(&self) -> bool { + if self.is_new() { + return true; + } + + self.server.has_changes() || self.deferred_changes.has_changes() + } + + /// + /// Commits the new object + /// + /// The changes done in [NewObject::deferred] will not be submitted yet, but the returned [Server] + /// object is preloaded with them. + /// + pub async fn commit(mut self) -> anyhow::Result { + let AttributeValue::String(hostname) = self.server.get("hostname") else { + return Err(anyhow::anyhow!("Required attribute 'hostname' is missing")); + }; + + if self.is_new() { + commit_changes(&Commit::new().create(self.server.attributes)).await?; + } else { + self.server.commit().await?; + } + + let mut server = Query::builder() + .filter("hostname", hostname) + .build() + .request() + .await? + .one()?; + + server.changes = self.deferred_changes; + + Ok(server) + } + + /// + /// Gets the initial commit data and the follow-up commit of deferred changes + /// + pub fn get_commit(self) -> (Commit, Commit) { + ( + Commit::new().create(self.server.attributes), + Commit::new().update(self.deferred_changes), + ) + } + + /// + /// The deferred method allows you to pre-update the newly created object + /// + /// It allows you to already prepare relations before other objects are created, making the new + /// object creation with relations a very simple 2-stage process. + /// + /// The input [Server] object for the `callback` is loaded with the [Dataset] of the current + /// object. Keep in mind though, that there are some attributes that are only filled after the + /// object is created + /// + pub fn deferred(&mut self, callback: impl FnOnce(&mut Server) -> R) -> R { + let mut server = Server { + object_id: 0, + attributes: self.server.attributes.clone(), + changes: std::mem::take(&mut self.deferred_changes), + }; + + let output = callback(&mut server); + + self.deferred_changes = std::mem::take(&mut server.changes); + + output + } +} + +impl Deref for NewObject { + type Target = Server; + + fn deref(&self) -> &Self::Target { + &self.server + } +} + +impl DerefMut for NewObject { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.server + } +} diff --git a/rust/src/query.rs b/rust/src/query.rs new file mode 100644 index 00000000..3c7ff84b --- /dev/null +++ b/rust/src/query.rs @@ -0,0 +1,95 @@ +use std::collections::HashSet; + +use crate::api::{commit_changes, query_objects, CommitResponse, QueryResponse, Server}; +use crate::commit::{Changeset, Commit}; +use crate::filter::{AttributeFilter, IntoFilterValue}; + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct Query { + pub filters: AttributeFilter, + pub restrict: HashSet, + #[serde(skip_serializing_if = "Option::is_none")] + pub order_by: Option, +} + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct QueryBuilder(Query); + +impl Query { + pub fn new() -> Self { + Default::default() + } + + pub fn builder() -> QueryBuilder { + Default::default() + } + + pub async fn request(&self) -> anyhow::Result { + query_objects(self).await + } + + pub async fn request_typed( + &self, + ) -> anyhow::Result> { + query_objects::(self).await + } +} + +impl QueryBuilder { + pub fn new() -> Self { + Default::default() + } + + pub fn filters(mut self, filter: AttributeFilter) -> Self { + self.0.filters.extend(filter); + + self + } + + pub fn filter( + mut self, + attribute: impl ToString, + value: impl IntoFilterValue + 'static, + ) -> Self { + self.0 + .filters + .insert(attribute.to_string(), value.into_filter_value()); + + self + } + + pub fn restrict>(mut self, attributes: I) -> Self { + self.0.restrict = HashSet::from_iter(attributes.into_iter().map(|v| v.to_string())); + + self + } + + pub fn order_by>>(mut self, value: T) -> Self { + self.0.order_by = value.into().as_ref().map(ToString::to_string); + + self + } + + pub fn build(mut self) -> Query { + if self.0.restrict.is_empty() { + self.0.restrict.insert(String::from("hostname")); + } + + self.0.restrict.insert(String::from("object_id")); + + self.0 + } +} + +impl Server { + pub async fn commit(&mut self) -> anyhow::Result { + let commit = Commit::new().update(self.changeset()); + self.changes = Changeset::default(); + + commit_changes(&commit).await + } + + pub fn has_changes(&self) -> bool { + self.changes.has_changes() + } +}