diff --git a/Cargo.lock b/Cargo.lock index 10e2f35..0dd5ba9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "bytes", "futures-core", "futures-sink", @@ -21,16 +21,16 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.11.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44cceded2fb55f3c4b67068fa64962e2ca59614edc5b03167de9ff82ae803da0" +checksum = "f860ee6746d0c5b682147b2f7f8ef036d4f92fe518251a3a35ffa3650eafdf0e" dependencies = [ "actix-codec", "actix-rt", "actix-service", "actix-utils", "base64 0.22.1", - "bitflags 2.9.4", + "bitflags 2.11.0", "brotli", "bytes", "bytestring", @@ -65,14 +65,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "actix-router" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7" dependencies = [ "bytestring", "cfg-if", @@ -104,7 +104,7 @@ dependencies = [ "actix-utils", "futures-core", "futures-util", - "mio 1.0.4", + "mio 1.1.1", "socket2 0.5.10", "tokio", "tracing", @@ -132,9 +132,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.11.0" +version = "4.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea" +checksum = "ff87453bc3b56e9b2b23c1cc0b1be8797184accf51d2abe0f8a33ec275d316bf" dependencies = [ "actix-codec", "actix-http", @@ -167,7 +167,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.5.10", + "socket2 0.6.3", "time", "tracing", "url", @@ -182,14 +182,14 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] @@ -218,7 +218,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -260,9 +260,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -288,12 +288,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -303,17 +297,11 @@ dependencies = [ "libc", ] -[[package]] -name = "ansi_term" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b3568b48b7cefa6b8ce125f9bb4989e52fbcc29ebea88df04cc7c5f12f70455" - [[package]] name = "anstream" -version = "0.6.20" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -326,9 +314,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" @@ -341,35 +329,38 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +dependencies = [ + "rustversion", +] [[package]] name = "askama" @@ -398,7 +389,7 @@ dependencies = [ "rustc-hash", "serde", "serde_derive", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -415,13 +406,12 @@ dependencies = [ [[package]] name = "assert_cmd" -version = "2.0.17" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66" +checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" dependencies = [ "anstyle", "bstr", - "doc-comment", "libc", "predicates", "predicates-core", @@ -478,7 +468,7 @@ dependencies = [ "thiserror 1.0.69", "time", "tokio", - "tokio-rustls 0.26.2", + "tokio-rustls 0.26.4", "tokio-stream", "tokio-util", "tokio-websockets", @@ -506,7 +496,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -517,7 +507,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -543,9 +533,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backon" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "592277618714fbcecda9a02ba7a8781f319d26532a88553bbacc77ba5d2b3a8d" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" dependencies = [ "fastrand", "gloo-timers", @@ -554,9 +544,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.75" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", @@ -564,7 +554,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -587,9 +577,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "basic-toml" @@ -619,11 +609,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -668,9 +658,9 @@ dependencies = [ [[package]] name = "bollard" -version = "0.19.2" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8796b390a5b4c86f9f2e8173a68c2791f4fa6b038b84e96dbc01c016d1e6722c" +checksum = "87a52479c9237eb04047ddb94788c41ca0d26eaff8b697ecfbb4c32f7fdc3b1b" dependencies = [ "base64 0.22.1", "bollard-stubs", @@ -678,9 +668,9 @@ dependencies = [ "futures-core", "futures-util", "hex", - "http 1.3.1", + "http 1.4.0", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-named-pipe", "hyper-util", "hyperlocal", @@ -691,7 +681,7 @@ dependencies = [ "serde_json", "serde_repr", "serde_urlencoded", - "thiserror 2.0.16", + "thiserror 2.0.18", "tokio", "tokio-util", "tower-service", @@ -701,9 +691,9 @@ dependencies = [ [[package]] name = "bollard-stubs" -version = "1.49.0-rc.28.3.3" +version = "1.49.1-rc.28.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7814991259013d5a5bee4ae28657dae0747d843cf06c40f7fc0c2894d6fa38" +checksum = "5731fe885755e92beff1950774068e0cae67ea6ec7587381536fca84f1779623" dependencies = [ "serde", "serde_json", @@ -728,41 +718,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "brocade-snmp-server" -version = "0.1.0" -dependencies = [ - "base64 0.22.1", - "brocade", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_secret", - "harmony_types", - "log", - "serde", - "tokio", - "url", -] - -[[package]] -name = "brocade-switch" -version = "0.1.0" -dependencies = [ - "async-trait", - "brocade", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "log", - "serde", - "tokio", - "url", -] - [[package]] name = "brotli" version = "8.0.2" @@ -786,9 +741,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "regex-automata", @@ -797,9 +752,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -809,18 +764,18 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] [[package]] name = "bytestring" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" +checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" dependencies = [ "bytes", ] @@ -833,11 +788,11 @@ checksum = "4964518bd3b4a8190e832886cdc0da9794f12e8e6c1613a9e90ff331c4c8724b" [[package]] name = "camino" -version = "1.1.12" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0b03af37dad7a14518b7691d81acb0f8222604ad3d1b02f6b4bed5188c0cd5" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -877,7 +832,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.18", ] [[package]] @@ -906,9 +861,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.35" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "590f9024a68a8c40351881787f1934dc11afd69090f5edb6831464694d836ea3" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "jobserver", @@ -916,27 +871,11 @@ dependencies = [ "shlex", ] -[[package]] -name = "cert_manager" -version = "0.1.0" -dependencies = [ - "assert_cmd", - "cidr", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "log", - "tokio", - "url", -] - [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -952,16 +891,26 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", ] [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", @@ -991,9 +940,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.47" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -1001,9 +950,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.47" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -1013,21 +962,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.47" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "clap_lex" -version = "0.7.5" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "color-eyre" @@ -1087,15 +1036,15 @@ dependencies = [ [[package]] name = "console" -version = "0.16.0" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d" +checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" dependencies = [ "encode_unicode", "libc", "once_cell", "unicode-width 0.2.0", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1119,7 +1068,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "once_cell", "tiny-keccak", ] @@ -1133,6 +1082,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" version = "0.16.2" @@ -1180,10 +1138,19 @@ dependencies = [ ] [[package]] -name = "crc" -version = "3.3.0" +name = "cpufeatures" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -1284,10 +1251,10 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "crossterm_winapi", "futures-core", - "mio 1.0.4", + "mio 1.1.1", "parking_lot", "rustix 0.38.44", "signal-hook", @@ -1324,9 +1291,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -1355,7 +1322,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -1372,7 +1339,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -1395,6 +1362,26 @@ dependencies = [ "darling_macro 0.20.11", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + [[package]] name = "darling_core" version = "0.14.4" @@ -1420,7 +1407,34 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.106", + "syn 2.0.117", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.117", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.117", ] [[package]] @@ -1442,14 +1456,36 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.106", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.117", ] [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "der" @@ -1464,12 +1500,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.3" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -1480,7 +1516,7 @@ checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -1489,7 +1525,16 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" dependencies = [ - "derive_builder_macro", + "derive_builder_macro 0.12.0", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro 0.20.2", ] [[package]] @@ -1504,34 +1549,58 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "derive_builder_macro" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" dependencies = [ - "derive_builder_core", + "derive_builder_core 0.12.0", "syn 1.0.109", ] [[package]] -name = "derive_more" -version = "2.0.1" +name = "derive_builder_macro" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core 0.20.2", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ + "convert_case 0.10.0", "proc-macro2", "quote", - "syn 2.0.106", + "rustc_version", + "syn 2.0.117", "unicode-xid", ] @@ -1586,7 +1655,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1597,7 +1666,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -1613,12 +1682,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - [[package]] name = "dockerfile_builder" version = "0.1.6" @@ -1639,7 +1702,7 @@ dependencies = [ "anyhow", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -1703,7 +1766,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -1762,29 +1825,29 @@ dependencies = [ [[package]] name = "enum-ordinalize" -version = "4.3.0" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" dependencies = [ "enum-ordinalize-derive", ] [[package]] name = "enum-ordinalize-derive" -version = "4.3.1" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "env_filter" -version = "0.1.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" dependencies = [ "log", "regex", @@ -1792,9 +1855,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" dependencies = [ "anstream", "anstyle", @@ -1811,22 +1874,23 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.6" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" dependencies = [ "serde", + "serde_core", "typeid", ] [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1865,443 +1929,6 @@ dependencies = [ name = "example" version = "0.0.0" -[[package]] -name = "example-application-monitoring-with-tenant" -version = "0.1.0" -dependencies = [ - "env_logger", - "harmony", - "harmony_cli", - "harmony_types", - "logging", - "tokio", - "url", -] - -[[package]] -name = "example-cli" -version = "0.1.0" -dependencies = [ - "assert_cmd", - "cidr", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "log", - "tokio", - "url", -] - -[[package]] -name = "example-k8s-drain-node" -version = "0.1.0" -dependencies = [ - "assert_cmd", - "cidr", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "inquire 0.7.5", - "log", - "tokio", - "url", -] - -[[package]] -name = "example-k8s-write-file-on-node" -version = "0.1.0" -dependencies = [ - "assert_cmd", - "cidr", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "inquire 0.7.5", - "log", - "tokio", - "url", -] - -[[package]] -name = "example-kube-rs" -version = "0.1.0" -dependencies = [ - "cidr", - "env_logger", - "harmony", - "harmony_macros", - "http 1.3.1", - "inquire 0.7.5", - "k8s-openapi", - "kube", - "log", - "serde_yaml", - "tokio", - "url", -] - -[[package]] -name = "example-lamp" -version = "0.1.0" -dependencies = [ - "cidr", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "log", - "tokio", - "url", -] - -[[package]] -name = "example-monitoring" -version = "0.1.0" -dependencies = [ - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "tokio", - "url", -] - -[[package]] -name = "example-monitoring-with-tenant" -version = "0.1.0" -dependencies = [ - "cidr", - "harmony", - "harmony_cli", - "harmony_types", - "tokio", - "url", -] - -[[package]] -name = "example-multisite-postgres" -version = "0.1.0" -dependencies = [ - "cidr", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "log", - "tokio", - "url", -] - -[[package]] -name = "example-nats" -version = "0.1.0" -dependencies = [ - "cidr", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "log", - "tokio", - "url", -] - -[[package]] -name = "example-nats-module-supercluster" -version = "0.1.0" -dependencies = [ - "cidr", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "k8s-openapi", - "log", - "tokio", - "url", -] - -[[package]] -name = "example-nats-supercluster" -version = "0.1.0" -dependencies = [ - "cidr", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "k8s-openapi", - "log", - "tokio", - "url", -] - -[[package]] -name = "example-node-health" -version = "0.1.0" -dependencies = [ - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "log", - "tokio", -] - -[[package]] -name = "example-ntfy" -version = "0.1.0" -dependencies = [ - "harmony", - "harmony_cli", - "tokio", - "url", -] - -[[package]] -name = "example-okd-cluster-alerts" -version = "0.1.0" -dependencies = [ - "brocade", - "cidr", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_secret", - "harmony_secret_derive", - "harmony_types", - "log", - "serde", - "tokio", - "url", -] - -[[package]] -name = "example-okd-install" -version = "0.1.0" -dependencies = [ - "brocade", - "cidr", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_secret", - "harmony_secret_derive", - "harmony_types", - "log", - "schemars 0.8.22", - "serde", - "tokio", - "url", -] - -[[package]] -name = "example-openbao" -version = "0.1.0" -dependencies = [ - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "tokio", - "url", -] - -[[package]] -name = "example-operatorhub-catalogsource" -version = "0.1.0" -dependencies = [ - "cidr", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "log", - "tokio", - "url", -] - -[[package]] -name = "example-opnsense" -version = "0.1.0" -dependencies = [ - "brocade", - "cidr", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_secret", - "harmony_types", - "log", - "schemars 0.8.22", - "serde", - "tokio", - "url", -] - -[[package]] -name = "example-opnsense-node-exporter" -version = "0.1.0" -dependencies = [ - "async-trait", - "cidr", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_secret", - "harmony_secret_derive", - "harmony_types", - "log", - "serde", - "tokio", - "url", -] - -[[package]] -name = "example-postgresql" -version = "0.1.0" -dependencies = [ - "cidr", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "log", - "tokio", - "url", -] - -[[package]] -name = "example-public-postgres" -version = "0.1.0" -dependencies = [ - "cidr", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "log", - "tokio", - "url", -] - -[[package]] -name = "example-pxe" -version = "0.1.0" -dependencies = [ - "brocade", - "cidr", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_secret", - "harmony_secret_derive", - "harmony_types", - "log", - "schemars 0.8.22", - "serde", - "tokio", - "url", -] - -[[package]] -name = "example-remove-rook-osd" -version = "0.1.0" -dependencies = [ - "harmony", - "harmony_cli", - "tokio", -] - -[[package]] -name = "example-rust" -version = "0.1.0" -dependencies = [ - "base64 0.22.1", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "log", - "tokio", - "url", -] - -[[package]] -name = "example-tenant" -version = "0.1.0" -dependencies = [ - "cidr", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "log", - "tokio", - "url", -] - -[[package]] -name = "example-try-rust-webapp" -version = "0.1.0" -dependencies = [ - "base64 0.22.1", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "log", - "tokio", - "url", -] - -[[package]] -name = "example-tui" -version = "0.1.0" -dependencies = [ - "cidr", - "env_logger", - "harmony", - "harmony_macros", - "harmony_tui", - "harmony_types", - "log", - "tokio", - "url", -] - -[[package]] -name = "example_validate_ceph_cluster_health" -version = "0.1.0" -dependencies = [ - "harmony", - "harmony_cli", - "tokio", -] - [[package]] name = "eyre" version = "0.6.12" @@ -2358,27 +1985,26 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.60.2", ] [[package]] name = "find-msvc-tools" -version = "0.1.0" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e178e4fba8a2726903f6ba98a6d221e76f9c12c650d5dc0e6afdc50677b49650" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -2446,9 +2072,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -2461,9 +2087,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -2471,15 +2097,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -2499,38 +2125,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -2540,7 +2166,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -2603,9 +2228,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -2623,11 +2248,37 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "getset" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "ghash" version = "0.5.1" @@ -2640,9 +2291,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "gloo-timers" @@ -2679,7 +2330,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.11.0", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -2688,17 +2339,17 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.3.1", - "indexmap 2.11.0", + "http 1.4.0", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -2717,7 +2368,7 @@ dependencies = [ "brocade", "chrono", "cidr", - "convert_case", + "convert_case 0.8.0", "derive-new", "directories", "dockerfile_builder", @@ -2726,6 +2377,7 @@ dependencies = [ "env_logger", "fqdn", "futures-util", + "harmony-k8s", "harmony_execution", "harmony_inventory_agent", "harmony_macros", @@ -2734,7 +2386,7 @@ dependencies = [ "harmony_types", "helm-wrapper-rs", "hex", - "http 1.3.1", + "http 1.4.0", "inquire 0.7.5", "k3d-rs", "k8s-openapi", @@ -2748,6 +2400,7 @@ dependencies = [ "opnsense-config-xml", "option-ext", "pretty_assertions", + "rand 0.9.2", "reqwest 0.11.27", "russh", "rust-ipmi", @@ -2765,7 +2418,7 @@ dependencies = [ "temp-dir", "temp-file", "tempfile", - "thiserror 2.0.16", + "thiserror 2.0.18", "tokio", "tokio-retry", "tokio-util", @@ -2774,6 +2427,25 @@ dependencies = [ "walkdir", ] +[[package]] +name = "harmony-k8s" +version = "0.1.0" +dependencies = [ + "inquire 0.7.5", + "k8s-openapi", + "kube", + "log", + "pretty_assertions", + "reqwest 0.12.28", + "serde", + "serde_json", + "serde_yaml", + "similar", + "tokio", + "tokio-retry", + "url", +] + [[package]] name = "harmony-node-readiness-endpoint" version = "0.1.0" @@ -2784,7 +2456,7 @@ dependencies = [ "k8s-openapi", "kube", "log", - "reqwest 0.12.23", + "reqwest 0.12.28", "serde", "serde_json", "tokio", @@ -2807,7 +2479,7 @@ dependencies = [ "pretty_assertions", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.18", "tokio", ] @@ -2875,7 +2547,7 @@ dependencies = [ "directories", "lazy_static", "log", - "thiserror 2.0.16", + "thiserror 2.0.18", ] [[package]] @@ -2889,27 +2561,14 @@ dependencies = [ "local-ip-address", "log", "mdns-sd 0.14.1 (git+https://github.com/jggc/mdns-sd.git?branch=patch-1)", - "reqwest 0.12.23", + "reqwest 0.12.28", "serde", "serde_json", "sysinfo", - "thiserror 2.0.16", + "thiserror 2.0.18", "tokio", ] -[[package]] -name = "harmony_inventory_builder" -version = "0.1.0" -dependencies = [ - "cidr", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "tokio", - "url", -] - [[package]] name = "harmony_macros" version = "0.1.0" @@ -2919,7 +2578,7 @@ dependencies = [ "quote", "serde", "serde_yaml", - "syn 2.0.106", + "syn 2.0.117", "url", ] @@ -2930,7 +2589,7 @@ dependencies = [ "async-trait", "directories", "harmony_secret_derive", - "http 1.3.1", + "http 1.4.0", "infisical", "inquire 0.7.5", "interactive-parse", @@ -2941,7 +2600,7 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror 2.0.16", + "thiserror 2.0.18", "tokio", "vaultrs", ] @@ -2953,7 +2612,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -3002,6 +2661,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashlink" version = "0.10.0" @@ -3020,7 +2685,7 @@ dependencies = [ "base64 0.22.1", "bytes", "headers-core", - "http 1.3.1", + "http 1.4.0", "httpdate", "mime", "sha1", @@ -3032,7 +2697,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http 1.3.1", + "http 1.4.0", ] [[package]] @@ -3051,7 +2716,7 @@ dependencies = [ "non-blank-string-rs", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.18", ] [[package]] @@ -3092,18 +2757,18 @@ dependencies = [ [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "hostname" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" dependencies = [ "cfg-if", "libc", @@ -3123,12 +2788,11 @@ dependencies = [ [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -3150,7 +2814,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.3.1", + "http 1.4.0", ] [[package]] @@ -3161,7 +2825,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "pin-project-lite", ] @@ -3180,18 +2844,18 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "httptest" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde82de3ef9bd882493c6a5edbc3363ad928925b30ccecc0f2ddeb42601b3021" +checksum = "a422b4c865d103368628ae1247be6159ad8041f803eb9e2176cf69ad7d13da40" dependencies = [ "bstr", "bytes", "crossbeam-channel", "form_urlencoded", "futures", - "http 1.3.1", + "http 1.4.0", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", "log", "once_cell", @@ -3228,16 +2892,16 @@ dependencies = [ [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", - "h2 0.4.12", - "http 1.3.1", + "h2 0.4.13", + "http 1.4.0", "http-body 1.0.1", "httparse", "httpdate", @@ -3258,14 +2922,14 @@ dependencies = [ "bytes", "futures-util", "headers", - "http 1.3.1", - "hyper 1.7.0", + "http 1.4.0", + "hyper 1.8.1", "hyper-rustls 0.27.7", "hyper-util", "pin-project-lite", "rustls-native-certs 0.7.3", "tokio", - "tokio-rustls 0.26.2", + "tokio-rustls 0.26.4", "tower-service", ] @@ -3276,7 +2940,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" dependencies = [ "hex", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", "pin-project-lite", "tokio", @@ -3304,17 +2968,17 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.3.1", - "hyper 1.7.0", + "http 1.4.0", + "hyper 1.8.1", "hyper-util", "log", - "rustls 0.23.31", - "rustls-native-certs 0.8.1", + "rustls 0.23.37", + "rustls-native-certs 0.8.3", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.2", + "tokio-rustls 0.26.4", "tower-service", - "webpki-roots 1.0.2", + "webpki-roots 1.0.6", ] [[package]] @@ -3323,7 +2987,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", "pin-project-lite", "tokio", @@ -3332,23 +2996,22 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", - "hyper 1.7.0", + "hyper 1.8.1", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -3362,7 +3025,7 @@ checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" dependencies = [ "hex", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", "pin-project-lite", "tokio", @@ -3371,9 +3034,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -3381,7 +3044,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.2", + "windows-core 0.62.2", ] [[package]] @@ -3395,9 +3058,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -3408,9 +3071,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -3421,11 +3084,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -3436,42 +3098,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -3479,6 +3137,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -3541,20 +3205,21 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "serde", + "serde_core", ] [[package]] name = "indicatif" -version = "0.18.0" +version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" dependencies = [ "console", "portable-atomic", @@ -3575,9 +3240,12 @@ dependencies = [ [[package]] name = "indoc" -version = "2.0.6" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] [[package]] name = "infisical" @@ -3585,7 +3253,7 @@ version = "0.0.2" source = "git+https://github.com/jggc/rust-sdk.git?branch=patch-1#30d820194d29491411bd14f6c2e18ec500bb0b14" dependencies = [ "base64 0.22.1", - "reqwest 0.12.23", + "reqwest 0.12.28", "serde", "serde_json", "thiserror 1.0.69", @@ -3625,7 +3293,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "crossterm 0.25.0", "dyn-clone", "fuzzy-matcher", @@ -3638,15 +3306,15 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.9" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" +checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" dependencies = [ - "darling 0.20.11", + "darling 0.23.0", "indoc", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -3665,28 +3333,17 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -3694,9 +3351,9 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -3709,32 +3366,32 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.15" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", - "serde", + "serde_core", ] [[package]] name = "jiff-static" -version = "0.2.15" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -3749,9 +3406,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -3759,9 +3416,9 @@ dependencies = [ [[package]] name = "json-patch" -version = "4.0.0" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "159294d661a039f7644cea7e4d844e6b25aaf71c1ffe9d73a96d768c24b0faf4" +checksum = "f300e415e2134745ef75f04562dd0145405c2f7fd92065db029ac4b16b57fe90" dependencies = [ "jsonptr", "serde", @@ -3769,26 +3426,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "json-prompt" -version = "0.1.0" -dependencies = [ - "brocade", - "cidr", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_secret", - "harmony_secret_derive", - "harmony_types", - "log", - "schemars 0.8.22", - "serde", - "tokio", - "url", -] - [[package]] name = "jsonpath-rust" version = "0.7.5" @@ -3799,7 +3436,7 @@ dependencies = [ "pest_derive", "regex", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.18", ] [[package]] @@ -3840,7 +3477,7 @@ dependencies = [ "octocrab", "pretty_assertions", "regex", - "reqwest 0.12.23", + "reqwest 0.12.28", "sha2", "tokio", "url", @@ -3883,10 +3520,10 @@ dependencies = [ "either", "futures", "home", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-http-proxy", "hyper-rustls 0.27.7", "hyper-timeout", @@ -3895,12 +3532,12 @@ dependencies = [ "k8s-openapi", "kube-core", "pem", - "rustls 0.23.31", + "rustls 0.23.37", "secrecy", "serde", "serde_json", "serde_yaml", - "thiserror 2.0.16", + "thiserror 2.0.18", "tokio", "tokio-tungstenite", "tokio-util", @@ -3918,14 +3555,14 @@ dependencies = [ "chrono", "derive_more", "form_urlencoded", - "http 1.3.1", + "http 1.4.0", "json-patch", "k8s-openapi", "schemars 0.8.22", "serde", "serde-value", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.18", ] [[package]] @@ -3939,7 +3576,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -3963,7 +3600,7 @@ dependencies = [ "pin-project", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.18", "tokio", "tokio-util", "tracing", @@ -3985,26 +3622,33 @@ dependencies = [ ] [[package]] -name = "libc" -version = "0.2.175" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.9" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "libc", - "redox_syscall", + "plain", + "redox_syscall 0.7.3", ] [[package]] @@ -4026,15 +3670,15 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "local-channel" @@ -4049,14 +3693,13 @@ dependencies = [ [[package]] name = "local-ip-address" -version = "0.6.5" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "656b3b27f8893f7bbf9485148ff9a65f019e3f33bd5cdc87c83cab16b3fd9ec8" +checksum = "79ef8c257c92ade496781a32a581d43e3d512cf8ce714ecf04ea80f93ed0ff4a" dependencies = [ "libc", "neli", - "thiserror 2.0.16", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4067,19 +3710,18 @@ checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "log-panics" @@ -4090,15 +3732,6 @@ dependencies = [ "log", ] -[[package]] -name = "logging" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "461a8beca676e8ab1bd468c92e9b4436d6368e11e96ae038209e520cfe665e46" -dependencies = [ - "ansi_term", -] - [[package]] name = "lru" version = "0.12.5" @@ -4155,9 +3788,9 @@ dependencies = [ "flume", "if-addrs", "log", - "mio 1.0.4", + "mio 1.1.1", "socket-pktinfo", - "socket2 0.6.0", + "socket2 0.6.3", ] [[package]] @@ -4169,16 +3802,16 @@ dependencies = [ "flume", "if-addrs", "log", - "mio 1.0.4", + "mio 1.1.1", "socket-pktinfo", - "socket2 0.6.0", + "socket2 0.6.3", ] [[package]] name = "memchr" -version = "2.7.5" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mime" @@ -4193,6 +3826,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -4209,39 +3843,43 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", "wasi", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "neli" -version = "0.6.5" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93062a0dce6da2517ea35f301dfc88184ce18d3601ec786a727a87bf535deca9" +checksum = "22f9786d56d972959e1408b6a93be6af13b9c1392036c5c1fafa08a1b0c6ee87" dependencies = [ + "bitflags 2.11.0", "byteorder", + "derive_builder 0.20.2", + "getset", "libc", "log", "neli-proc-macros", + "parking_lot", ] [[package]] name = "neli-proc-macros" -version = "0.1.4" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8034b7fbb6f9455b2a96c19e6edf8dc9fc34c70449938d8ee3b4df363f61fe" +checksum = "05d8d08c6e98f20a62417478ebf7be8e1425ec9acecc6f63e22da633f6b71609" dependencies = [ "either", "proc-macro2", "quote", "serde", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -4271,7 +3909,7 @@ dependencies = [ "data-encoding", "ed25519", "ed25519-dalek", - "getrandom 0.2.16", + "getrandom 0.2.17", "log", "rand 0.8.5", "signatory", @@ -4288,9 +3926,9 @@ dependencies = [ [[package]] name = "ntapi" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" dependencies = [ "winapi 0.3.9", ] @@ -4317,11 +3955,10 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ - "byteorder", "lazy_static", "libm", "num-integer", @@ -4334,9 +3971,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-integer" @@ -4380,9 +4017,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] @@ -4402,10 +4039,10 @@ dependencies = [ "either", "futures", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-rustls 0.27.7", "hyper-timeout", "hyper-util", @@ -4435,9 +4072,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "opaque-debug" @@ -4451,6 +4088,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "opnsense-config" version = "0.1.0" @@ -4485,7 +4128,7 @@ dependencies = [ "pretty_assertions", "rand 0.9.2", "serde", - "thiserror 2.0.16", + "thiserror 2.0.18", "tokio", "uuid", "xml-rs", @@ -4510,9 +4153,9 @@ dependencies = [ [[package]] name = "owo-colors" -version = "4.2.2" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] name = "p256" @@ -4560,9 +4203,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -4570,15 +4213,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -4622,12 +4265,12 @@ dependencies = [ [[package]] name = "pem" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ "base64 0.22.1", - "serde", + "serde_core", ] [[package]] @@ -4647,20 +4290,19 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.1" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ "memchr", - "thiserror 2.0.16", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.8.1" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" dependencies = [ "pest", "pest_generator", @@ -4668,22 +4310,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.1" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "pest_meta" -version = "2.8.1" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", "sha2", @@ -4691,29 +4333,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -4765,13 +4407,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "poly1305" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -4783,31 +4431,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -4829,9 +4477,9 @@ dependencies = [ [[package]] name = "predicates" -version = "3.1.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" dependencies = [ "anstyle", "difflib", @@ -4840,15 +4488,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" [[package]] name = "predicates-tree" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" dependencies = [ "predicates-core", "termtree", @@ -4864,6 +4512,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -4875,18 +4533,40 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit", + "toml_edit 0.25.4+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -4909,9 +4589,9 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.31", - "socket2 0.6.0", - "thiserror 2.0.16", + "rustls 0.23.37", + "socket2 0.6.3", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -4929,10 +4609,10 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.31", + "rustls 0.23.37", "rustls-pki-types", "slab", - "thiserror 2.0.16", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -4947,16 +4627,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.0", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -4967,6 +4647,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -4991,7 +4677,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20 0.10.0", + "getrandom 0.4.2", + "rand_core 0.10.0", ] [[package]] @@ -5011,7 +4708,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -5020,25 +4717,31 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + [[package]] name = "ratatui" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "cassowary", "compact_str", "crossterm 0.28.1", @@ -5075,11 +4778,20 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.0", ] [[package]] @@ -5088,38 +4800,38 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", - "thiserror 2.0.16", + "thiserror 2.0.18", ] [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "regex" -version = "1.11.3" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ - "aho-corasick 1.1.3", + "aho-corasick 1.1.4", "memchr", "regex-automata", "regex-syntax", @@ -5127,26 +4839,26 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ - "aho-corasick 1.1.3", + "aho-corasick 1.1.4", "memchr", "regex-syntax", ] [[package]] name = "regex-lite" -version = "0.1.7" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -5191,20 +4903,20 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.23" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", "futures-core", "futures-util", - "h2 0.4.12", - "http 1.3.1", + "h2 0.4.13", + "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-rustls 0.27.7", "hyper-util", "js-sys", @@ -5212,14 +4924,14 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.31", + "rustls 0.23.37", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper 1.0.2", "tokio", - "tokio-rustls 0.26.2", + "tokio-rustls 0.26.4", "tokio-util", "tower", "tower-http", @@ -5229,7 +4941,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.2", + "webpki-roots 1.0.6", ] [[package]] @@ -5242,21 +4954,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "rhob-application-monitoring" -version = "0.1.0" -dependencies = [ - "base64 0.22.1", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "log", - "tokio", - "url", -] - [[package]] name = "ring" version = "0.17.14" @@ -5265,7 +4962,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -5273,9 +4970,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.8" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ "const-oid", "digest", @@ -5301,10 +4998,10 @@ dependencies = [ "aes", "aes-gcm", "async-trait", - "bitflags 2.9.4", + "bitflags 2.11.0", "byteorder", "cbc", - "chacha20", + "chacha20 0.9.1", "ctr", "curve25519-dalek", "des", @@ -5402,13 +5099,13 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bb94393cafad0530145b8f626d8687f1ee1dedb93d7ba7740d6ae81868b13b5" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "bytes", "chrono", "flurry", "log", "serde", - "thiserror 2.0.16", + "thiserror 2.0.18", "tokio", "tokio-util", ] @@ -5430,9 +5127,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" @@ -5458,8 +5155,8 @@ dependencies = [ "anyhow", "async-trait", "bytes", - "http 1.3.1", - "reqwest 0.12.23", + "http 1.4.0", + "reqwest 0.12.28", "rustify_derive", "serde", "serde_json", @@ -5489,7 +5186,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -5498,15 +5195,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", ] [[package]] @@ -5523,15 +5220,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.31" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.4", + "rustls-webpki 0.103.9", "subtle", "zeroize", ] @@ -5542,7 +5239,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ - "openssl-probe", + "openssl-probe 0.1.6", "rustls-pemfile 2.2.0", "rustls-pki-types", "schannel", @@ -5551,14 +5248,14 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe", + "openssl-probe 0.2.1", "rustls-pki-types", "schannel", - "security-framework 3.3.0", + "security-framework 3.7.0", ] [[package]] @@ -5581,9 +5278,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -5611,9 +5308,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "ring", "rustls-pki-types", @@ -5628,9 +5325,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "salsa20" @@ -5652,11 +5349,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5685,9 +5382,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", @@ -5704,7 +5401,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -5763,7 +5460,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -5772,11 +5469,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.3.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -5785,9 +5482,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -5801,30 +5498,33 @@ checksum = "689224d06523904ebcc9b482c6a3f4f7fb396096645c4cd10c0d2ff7371a34d3" [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" dependencies = [ "serde", + "serde_core", ] [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] [[package]] name = "serde-untagged" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34836a629bcbc6f1afdf0907a744870039b1e14c0561cb26094fa683b158eff3" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" dependencies = [ "erased-serde", "serde", + "serde_core", "typeid", ] @@ -5839,14 +5539,23 @@ dependencies = [ ] [[package]] -name = "serde_derive" -version = "1.0.219" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -5857,19 +5566,20 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -5883,12 +5593,13 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ "itoa", "serde", + "serde_core", ] [[package]] @@ -5899,7 +5610,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -5913,14 +5624,14 @@ dependencies = [ [[package]] name = "serde_tokenstream" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64060d864397305347a78851c51588fd283767e7e7589829e8121d65512340f1" +checksum = "d7c49585c52c01f13c5c2ebb333f14f6885d76daa768d8a037d28017ec538c69" dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -5937,19 +5648,18 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" +checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.0", + "indexmap 2.13.0", "schemars 0.9.0", - "schemars 1.0.4", - "serde", - "serde_derive", + "schemars 1.2.1", + "serde_core", "serde_json", "serde_with_macros", "time", @@ -5957,14 +5667,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" +checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" dependencies = [ - "darling 0.20.11", + "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -5973,7 +5683,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.11.0", + "indexmap 2.13.0", "itoa", "ryu", "serde", @@ -5987,7 +5697,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -5998,7 +5708,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -6029,22 +5739,23 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio 0.8.11", - "mio 1.0.4", + "mio 1.1.1", "signal-hook", ] [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -6070,6 +5781,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "similar" version = "2.7.0" @@ -6078,21 +5795,21 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "simple_asn1" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.16", + "thiserror 2.0.18", "time", ] [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -6105,23 +5822,23 @@ dependencies = [ [[package]] name = "snafu" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4800ae0e2ebdfaea32ffb9745642acdc378740dcbd74d3fb3cd87572a34810c6" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" dependencies = [ "snafu-derive", ] [[package]] name = "snafu-derive" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186f5ba9999528053fb497fdf0dd330efcc69cfe4ad03776c9d704bc54fee10f" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -6131,7 +5848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "927136cc2ae6a1b0e66ac6b1210902b75c3f726db004a73bc18686dcd0dcd22f" dependencies = [ "libc", - "socket2 0.6.0", + "socket2 0.6.3", "windows-sys 0.60.2", ] @@ -6147,12 +5864,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6205,7 +5922,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink", - "indexmap 2.11.0", + "indexmap 2.13.0", "log", "memchr", "once_cell", @@ -6214,7 +5931,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.16", + "thiserror 2.0.18", "tokio", "tokio-stream", "tracing", @@ -6231,7 +5948,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -6254,7 +5971,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.106", + "syn 2.0.117", "tokio", "url", ] @@ -6267,7 +5984,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.4", + "bitflags 2.11.0", "byteorder", "bytes", "crc", @@ -6296,7 +6013,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.16", + "thiserror 2.0.18", "tracing", "whoami", ] @@ -6309,7 +6026,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.4", + "bitflags 2.11.0", "byteorder", "crc", "dotenvy", @@ -6333,7 +6050,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.16", + "thiserror 2.0.18", "tracing", "whoami", ] @@ -6357,7 +6074,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.16", + "thiserror 2.0.18", "tracing", "url", ] @@ -6371,7 +6088,7 @@ dependencies = [ "aes", "aes-gcm", "cbc", - "chacha20", + "chacha20 0.9.1", "cipher", "ctr", "poly1305", @@ -6415,9 +6132,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -6476,7 +6193,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -6488,27 +6205,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.106", -] - -[[package]] -name = "sttest" -version = "0.1.0" -dependencies = [ - "brocade", - "cidr", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_secret", - "harmony_secret_derive", - "harmony_types", - "log", - "schemars 0.8.22", - "serde", - "tokio", - "url", + "syn 2.0.117", ] [[package]] @@ -6530,9 +6227,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -6574,7 +6271,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -6644,15 +6341,15 @@ checksum = "b5ff282c3f91797f0acb021f3af7fffa8a78601f0f2fd0a9f79ee7dcf9a9af9e" [[package]] name = "tempfile" -version = "3.21.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", - "rustix 1.0.8", - "windows-sys 0.60.2", + "rustix 1.1.4", + "windows-sys 0.61.2", ] [[package]] @@ -6672,11 +6369,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.18", ] [[package]] @@ -6687,18 +6384,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -6712,29 +6409,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.43" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", + "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -6751,9 +6449,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -6776,33 +6474,30 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", - "mio 1.0.4", + "mio 1.1.1", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", - "socket2 0.6.0", + "socket2 0.6.3", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -6828,19 +6523,19 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.31", + "rustls 0.23.37", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -6861,9 +6556,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -6883,13 +6578,13 @@ dependencies = [ "bytes", "futures-core", "futures-sink", - "http 1.3.1", + "http 1.4.0", "httparse", "rand 0.8.5", "ring", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.2", + "tokio-rustls 0.26.4", "tokio-util", "webpki-roots 0.26.11", ] @@ -6902,8 +6597,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] @@ -6915,20 +6610,50 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.11.0", + "indexmap 2.13.0", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", "toml_write", "winnow", ] +[[package]] +name = "toml_edit" +version = "0.25.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 1.0.0+spec-1.1.0", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow", +] + [[package]] name = "toml_write" version = "0.1.2" @@ -6954,15 +6679,15 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "base64 0.22.1", - "bitflags 2.9.4", + "bitflags 2.11.0", "bytes", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "iri-string", "mime", @@ -6987,9 +6712,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -6999,20 +6724,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -7030,9 +6755,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "sharded-slab", "thread_local", @@ -7078,12 +6803,12 @@ checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" dependencies = [ "bytes", "data-encoding", - "http 1.3.1", + "http 1.4.0", "httparse", "log", "rand 0.9.2", "sha1", - "thiserror 2.0.16", + "thiserror 2.0.18", "utf-8", ] @@ -7095,9 +6820,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "ucd-trie" @@ -7113,24 +6838,24 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" @@ -7169,9 +6894,9 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "unit-prefix" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" [[package]] name = "universal-hash" @@ -7197,14 +6922,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -7227,28 +6953,16 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.2", "js-sys", - "rand 0.9.2", - "uuid-macro-internal", + "rand 0.10.0", "wasm-bindgen", ] -[[package]] -name = "uuid-macro-internal" -version = "1.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9384a660318abfbd7f8932c34d67e4d1ec511095f95972ddc01e19d7ba8413f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "valuable" version = "0.1.1" @@ -7263,9 +6977,9 @@ checksum = "f81eb4d9221ca29bad43d4b6871b6d2e7656e1af2cfca624a87e5d17880d831d" dependencies = [ "async-trait", "bytes", - "derive_builder", - "http 1.3.1", - "reqwest 0.12.23", + "derive_builder 0.12.0", + "http 1.4.0", + "reqwest 0.12.28", "rustify", "rustify_derive", "serde", @@ -7330,6 +7044,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasite" version = "0.1.0" @@ -7338,37 +7061,25 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -7377,9 +7088,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7387,26 +7098,48 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", - "wasm-bindgen-backend", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -7421,10 +7154,22 @@ dependencies = [ ] [[package]] -name = "web-sys" -version = "0.3.77" +name = "wasmparser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -7453,14 +7198,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.2", + "webpki-roots 1.0.6", ] [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -7499,11 +7244,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7533,9 +7278,9 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", @@ -7546,46 +7291,46 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" -version = "0.3.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] @@ -7623,7 +7368,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", ] [[package]] @@ -7659,19 +7413,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -7688,9 +7442,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -7706,9 +7460,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -7724,9 +7478,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -7736,9 +7490,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -7754,9 +7508,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -7772,9 +7526,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -7790,9 +7544,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -7808,15 +7562,15 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] @@ -7836,12 +7590,94 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wyz" @@ -7854,19 +7690,19 @@ dependencies = [ [[package]] name = "xattr" -version = "1.5.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.0.8", + "rustix 1.1.4", ] [[package]] name = "xml-rs" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" [[package]] name = "yansi" @@ -7894,17 +7730,16 @@ dependencies = [ "quote", "serde", "serde_tokenstream", - "syn 2.0.106", + "syn 2.0.117", "xml-rs", ] [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -7912,34 +7747,34 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", "synstructure 0.13.2", ] [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -7959,21 +7794,21 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", "synstructure 0.13.2", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -7982,9 +7817,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -7993,15 +7828,21 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + [[package]] name = "zstd" version = "0.13.3" @@ -8022,9 +7863,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.15+zstd.1.5.7" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 87396f3..8f524d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,10 +16,9 @@ members = [ "harmony_secret_derive", "harmony_secret", "adr/agent_discovery/mdns", - "brocade", - "harmony_agent", - "harmony_agent/deploy", "harmony_node_readiness", - "examples/*", + "brocade", + "harmony_agent", + "harmony_agent/deploy", "harmony_node_readiness", "harmony-k8s", ] [workspace.package] @@ -38,6 +37,8 @@ tokio = { version = "1.40", features = [ "macros", "rt-multi-thread", ] } +tokio-retry = "0.3.0" +tokio-util = "0.7.15" cidr = { features = ["serde"], version = "0.2" } russh = "0.45" russh-keys = "0.45" diff --git a/brocade/src/fast_iron.rs b/brocade/src/fast_iron.rs index cd425dc..371c265 100644 --- a/brocade/src/fast_iron.rs +++ b/brocade/src/fast_iron.rs @@ -1,8 +1,7 @@ use super::BrocadeClient; use crate::{ BrocadeInfo, Error, ExecutionMode, InterSwitchLink, InterfaceInfo, MacAddressEntry, - PortChannelId, PortOperatingMode, SecurityLevel, parse_brocade_mac_address, - shell::BrocadeShell, + PortChannelId, PortOperatingMode, parse_brocade_mac_address, shell::BrocadeShell, }; use async_trait::async_trait; diff --git a/brocade/src/network_operating_system.rs b/brocade/src/network_operating_system.rs index 994dbee..fa99bf6 100644 --- a/brocade/src/network_operating_system.rs +++ b/brocade/src/network_operating_system.rs @@ -8,7 +8,7 @@ use regex::Regex; use crate::{ BrocadeClient, BrocadeInfo, Error, ExecutionMode, InterSwitchLink, InterfaceInfo, InterfaceStatus, InterfaceType, MacAddressEntry, PortChannelId, PortOperatingMode, - SecurityLevel, parse_brocade_mac_address, shell::BrocadeShell, + parse_brocade_mac_address, shell::BrocadeShell, }; #[derive(Debug)] diff --git a/examples/cert_manager/src/main.rs b/examples/cert_manager/src/main.rs index e024725..1aa2655 100644 --- a/examples/cert_manager/src/main.rs +++ b/examples/cert_manager/src/main.rs @@ -1,8 +1,8 @@ use harmony::{ inventory::Inventory, modules::cert_manager::{ - capability::CertificateManagementConfig, score_cert_management::CertificateManagementScore, - score_certificate::CertificateScore, score_issuer::CertificateIssuerScore, + capability::CertificateManagementConfig, score_certificate::CertificateScore, + score_issuer::CertificateIssuerScore, }, topology::K8sAnywhereTopology, }; diff --git a/examples/k8s_drain_node/Cargo.toml b/examples/k8s_drain_node/Cargo.toml index d8ded7f..c804a8e 100644 --- a/examples/k8s_drain_node/Cargo.toml +++ b/examples/k8s_drain_node/Cargo.toml @@ -10,9 +10,10 @@ publish = false harmony = { path = "../../harmony" } harmony_cli = { path = "../../harmony_cli" } harmony_types = { path = "../../harmony_types" } +harmony_macros = { path = "../../harmony_macros" } +harmony-k8s = { path = "../../harmony-k8s" } cidr.workspace = true tokio.workspace = true -harmony_macros = { path = "../../harmony_macros" } log.workspace = true env_logger.workspace = true url.workspace = true diff --git a/examples/k8s_drain_node/src/main.rs b/examples/k8s_drain_node/src/main.rs index 71cf4b3..5720fe1 100644 --- a/examples/k8s_drain_node/src/main.rs +++ b/examples/k8s_drain_node/src/main.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use harmony::topology::k8s::{DrainOptions, K8sClient}; +use harmony_k8s::{DrainOptions, K8sClient}; use log::{info, trace}; #[tokio::main] diff --git a/examples/k8s_write_file_on_node/Cargo.toml b/examples/k8s_write_file_on_node/Cargo.toml index b735441..96bd344 100644 --- a/examples/k8s_write_file_on_node/Cargo.toml +++ b/examples/k8s_write_file_on_node/Cargo.toml @@ -10,9 +10,10 @@ publish = false harmony = { path = "../../harmony" } harmony_cli = { path = "../../harmony_cli" } harmony_types = { path = "../../harmony_types" } +harmony_macros = { path = "../../harmony_macros" } +harmony-k8s = { path = "../../harmony-k8s" } cidr.workspace = true tokio.workspace = true -harmony_macros = { path = "../../harmony_macros" } log.workspace = true env_logger.workspace = true url.workspace = true diff --git a/examples/k8s_write_file_on_node/src/main.rs b/examples/k8s_write_file_on_node/src/main.rs index f37e171..eb88b83 100644 --- a/examples/k8s_write_file_on_node/src/main.rs +++ b/examples/k8s_write_file_on_node/src/main.rs @@ -1,4 +1,4 @@ -use harmony::topology::k8s::{DrainOptions, K8sClient, NodeFile}; +use harmony_k8s::{K8sClient, NodeFile}; use log::{info, trace}; #[tokio::main] diff --git a/examples/openbao/src/main.rs b/examples/openbao/src/main.rs index 1d5653b..200d0f7 100644 --- a/examples/openbao/src/main.rs +++ b/examples/openbao/src/main.rs @@ -5,7 +5,7 @@ use harmony::{ #[tokio::main] async fn main() { let openbao = OpenbaoScore { - host: String::new(), + host: "openbao.sebastien.sto1.nationtech.io".to_string(), }; harmony_cli::run( diff --git a/examples/operatorhub_catalog/src/main.rs b/examples/operatorhub_catalog/src/main.rs index 8e35024..e4e3ac6 100644 --- a/examples/operatorhub_catalog/src/main.rs +++ b/examples/operatorhub_catalog/src/main.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - use harmony::{ inventory::Inventory, modules::{k8s::apps::OperatorHubCatalogSourceScore, postgresql::CloudNativePgOperatorScore}, @@ -9,7 +7,7 @@ use harmony::{ #[tokio::main] async fn main() { let operatorhub_catalog = OperatorHubCatalogSourceScore::default(); - let cnpg_operator = CloudNativePgOperatorScore::default(); + let cnpg_operator = CloudNativePgOperatorScore::default_openshift(); harmony_cli::run( Inventory::autoload(), diff --git a/examples/opnsense_node_exporter/src/main.rs b/examples/opnsense_node_exporter/src/main.rs index d71d2ed..75480e8 100644 --- a/examples/opnsense_node_exporter/src/main.rs +++ b/examples/opnsense_node_exporter/src/main.rs @@ -1,22 +1,13 @@ -use std::{ - net::{IpAddr, Ipv4Addr}, - sync::Arc, -}; +use std::sync::Arc; use async_trait::async_trait; -use cidr::Ipv4Cidr; use harmony::{ executors::ExecutorError, - hardware::{HostCategory, Location, PhysicalHost, SwitchGroup}, - infra::opnsense::OPNSenseManagementInterface, inventory::Inventory, modules::opnsense::node_exporter::NodeExporterScore, - topology::{ - HAClusterTopology, LogicalHost, PreparationError, PreparationOutcome, Topology, - UnmanagedRouter, node_exporter::NodeExporter, - }, + topology::{PreparationError, PreparationOutcome, Topology, node_exporter::NodeExporter}, }; -use harmony_macros::{ip, ipv4, mac_address}; +use harmony_macros::ip; #[derive(Debug)] struct OpnSenseTopology { diff --git a/examples/public_postgres/src/main.rs b/examples/public_postgres/src/main.rs index 029080e..772e801 100644 --- a/examples/public_postgres/src/main.rs +++ b/examples/public_postgres/src/main.rs @@ -1,8 +1,7 @@ use harmony::{ inventory::Inventory, modules::postgresql::{ - K8sPostgreSQLScore, PostgreSQLConnectionScore, PublicPostgreSQLScore, - capability::PostgreSQLConfig, + PostgreSQLConnectionScore, PublicPostgreSQLScore, capability::PostgreSQLConfig, }, topology::K8sAnywhereTopology, }; diff --git a/examples/rhob_application_monitoring/src/main.rs b/examples/rhob_application_monitoring/src/main.rs index 6eeaea2..a2596fa 100644 --- a/examples/rhob_application_monitoring/src/main.rs +++ b/examples/rhob_application_monitoring/src/main.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, path::PathBuf, sync::Arc}; +use std::{path::PathBuf, sync::Arc}; use harmony::{ inventory::Inventory, diff --git a/examples/rust/src/main.rs b/examples/rust/src/main.rs index a100606..f100b41 100644 --- a/examples/rust/src/main.rs +++ b/examples/rust/src/main.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, path::PathBuf, sync::Arc}; +use std::{path::PathBuf, sync::Arc}; use harmony::{ inventory::Inventory, diff --git a/examples/zitadel/Cargo.toml b/examples/zitadel/Cargo.toml new file mode 100644 index 0000000..9e7a7e8 --- /dev/null +++ b/examples/zitadel/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "example-zitadel" +edition = "2024" +version.workspace = true +readme.workspace = true +license.workspace = true + +[dependencies] +harmony = { path = "../../harmony" } +harmony_cli = { path = "../../harmony_cli" } +harmony_macros = { path = "../../harmony_macros" } +harmony_types = { path = "../../harmony_types" } +tokio.workspace = true +url.workspace = true diff --git a/examples/zitadel/src/main.rs b/examples/zitadel/src/main.rs new file mode 100644 index 0000000..e2d24ba --- /dev/null +++ b/examples/zitadel/src/main.rs @@ -0,0 +1,20 @@ +use harmony::{ + inventory::Inventory, modules::zitadel::ZitadelScore, topology::K8sAnywhereTopology, +}; + +#[tokio::main] +async fn main() { + let zitadel = ZitadelScore { + host: "sso.sto1.nationtech.io".to_string(), + zitadel_version: "v4.12.1".to_string(), + }; + + harmony_cli::run( + Inventory::autoload(), + K8sAnywhereTopology::from_env(), + vec![Box::new(zitadel)], + None, + ) + .await + .unwrap(); +} diff --git a/examples/zitadel/zitadel-9.24.0.tgz b/examples/zitadel/zitadel-9.24.0.tgz new file mode 100644 index 0000000..bf60892 Binary files /dev/null and b/examples/zitadel/zitadel-9.24.0.tgz differ diff --git a/harmony-k8s/Cargo.toml b/harmony-k8s/Cargo.toml new file mode 100644 index 0000000..f989c9c --- /dev/null +++ b/harmony-k8s/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "harmony-k8s" +edition = "2024" +version.workspace = true +readme.workspace = true +license.workspace = true + +[dependencies] +kube.workspace = true +k8s-openapi.workspace = true +tokio.workspace = true +tokio-retry.workspace = true +serde.workspace = true +serde_json.workspace = true +serde_yaml.workspace = true +log.workspace = true +similar.workspace = true +reqwest.workspace = true +url.workspace = true +inquire.workspace = true + +[dev-dependencies] +pretty_assertions.workspace = true diff --git a/harmony-k8s/src/apply.rs b/harmony-k8s/src/apply.rs new file mode 100644 index 0000000..cf1e7bf --- /dev/null +++ b/harmony-k8s/src/apply.rs @@ -0,0 +1,593 @@ +use kube::{ + Client, Error, Resource, + api::{ + Api, ApiResource, DynamicObject, GroupVersionKind, Patch, PatchParams, PostParams, + ResourceExt, + }, + core::ErrorResponse, + discovery::Scope, + error::DiscoveryError, +}; +use log::{debug, error, trace, warn}; +use serde::{Serialize, de::DeserializeOwned}; +use serde_json::Value; +use similar::TextDiff; +use url::Url; + +use crate::client::K8sClient; +use crate::helper; +use crate::types::WriteMode; + +/// The field-manager token sent with every server-side apply request. +pub const FIELD_MANAGER: &str = "harmony-k8s"; + +// ── Private helpers ────────────────────────────────────────────────────────── + +/// Serialise any `Serialize` payload to a [`DynamicObject`] via JSON. +fn to_dynamic(payload: &T) -> Result { + serde_json::from_value(serde_json::to_value(payload).map_err(Error::SerdeError)?) + .map_err(Error::SerdeError) +} + +/// Fetch the current resource, display a unified diff against `payload`, and +/// return `()`. All output goes to stdout (same behaviour as before). +/// +/// A 404 is treated as "resource would be created" — not an error. +async fn show_dry_run( + api: &Api, + name: &str, + payload: &T, +) -> Result<(), Error> { + let new_yaml = serde_yaml::to_string(payload) + .unwrap_or_else(|_| "Failed to serialize new resource".to_string()); + + match api.get(name).await { + Ok(current) => { + println!("\nDry-run for resource: '{name}'"); + let mut current_val = serde_yaml::to_value(¤t).unwrap_or(serde_yaml::Value::Null); + if let Some(map) = current_val.as_mapping_mut() { + map.remove(&serde_yaml::Value::String("status".to_string())); + } + let current_yaml = serde_yaml::to_string(¤t_val) + .unwrap_or_else(|_| "Failed to serialize current resource".to_string()); + + if current_yaml == new_yaml { + println!("No changes detected."); + } else { + println!("Changes detected:"); + let diff = TextDiff::from_lines(¤t_yaml, &new_yaml); + for change in diff.iter_all_changes() { + let sign = match change.tag() { + similar::ChangeTag::Delete => "-", + similar::ChangeTag::Insert => "+", + similar::ChangeTag::Equal => " ", + }; + print!("{sign}{change}"); + } + } + Ok(()) + } + Err(Error::Api(ErrorResponse { code: 404, .. })) => { + println!("\nDry-run for new resource: '{name}'"); + println!("Resource does not exist. Would be created:"); + for line in new_yaml.lines() { + println!("+{line}"); + } + Ok(()) + } + Err(e) => { + error!("Failed to fetch resource '{name}' for dry-run: {e}"); + Err(e) + } + } +} + +/// Execute the real (non-dry-run) apply, respecting [`WriteMode`]. +async fn do_apply( + api: &Api, + name: &str, + payload: &T, + patch_params: &PatchParams, + write_mode: &WriteMode, +) -> Result { + match write_mode { + WriteMode::CreateOrUpdate => { + // TODO refactor this arm to perform self.update and if fail with 404 self.create + // This will avoid the repetition of the api.patch and api.create calls within this + // function body. This makes the code more maintainable + match api.patch(name, patch_params, &Patch::Apply(payload)).await { + Ok(obj) => Ok(obj), + Err(Error::Api(ErrorResponse { code: 404, .. })) => { + debug!("Resource '{name}' not found via SSA, falling back to POST"); + let dyn_obj = to_dynamic(payload)?; + api.create(&PostParams::default(), &dyn_obj) + .await + .map_err(|e| { + error!("Failed to create '{name}': {e}"); + e + }) + } + Err(e) => { + error!("Failed to apply '{name}': {e}"); + Err(e) + } + } + } + WriteMode::Create => { + let dyn_obj = to_dynamic(payload)?; + api.create(&PostParams::default(), &dyn_obj) + .await + .map_err(|e| { + error!("Failed to create '{name}': {e}"); + e + }) + } + WriteMode::Update => match api.patch(name, patch_params, &Patch::Apply(payload)).await { + Ok(obj) => Ok(obj), + Err(Error::Api(ErrorResponse { code: 404, .. })) => Err(Error::Api(ErrorResponse { + code: 404, + message: format!("Resource '{name}' not found and WriteMode is UpdateOnly"), + reason: "NotFound".to_string(), + status: "Failure".to_string(), + })), + Err(e) => { + error!("Failed to update '{name}': {e}"); + Err(e) + } + }, + } +} + +// ── Public API ─────────────────────────────────────────────────────────────── + +impl K8sClient { + /// Server-side apply: create if absent, update if present. + /// Equivalent to `kubectl apply`. + pub async fn apply(&self, resource: &K, namespace: Option<&str>) -> Result + where + K: Resource + Clone + std::fmt::Debug + DeserializeOwned + Serialize, + ::DynamicType: Default, + { + self.apply_with_strategy(resource, namespace, WriteMode::CreateOrUpdate) + .await + } + + /// POST only — returns an error if the resource already exists. + pub async fn create(&self, resource: &K, namespace: Option<&str>) -> Result + where + K: Resource + Clone + std::fmt::Debug + DeserializeOwned + Serialize, + ::DynamicType: Default, + { + self.apply_with_strategy(resource, namespace, WriteMode::Create) + .await + } + + /// Server-side apply only — returns an error if the resource does not exist. + pub async fn update(&self, resource: &K, namespace: Option<&str>) -> Result + where + K: Resource + Clone + std::fmt::Debug + DeserializeOwned + Serialize, + ::DynamicType: Default, + { + self.apply_with_strategy(resource, namespace, WriteMode::Update) + .await + } + + pub async fn apply_with_strategy( + &self, + resource: &K, + namespace: Option<&str>, + write_mode: WriteMode, + ) -> Result + where + K: Resource + Clone + std::fmt::Debug + DeserializeOwned + Serialize, + ::DynamicType: Default, + { + debug!( + "apply_with_strategy: {:?} ns={:?}", + resource.meta().name, + namespace + ); + trace!("{:#}", serde_json::to_value(resource).unwrap_or_default()); + + let dyntype = K::DynamicType::default(); + let gvk = GroupVersionKind { + group: K::group(&dyntype).to_string(), + version: K::version(&dyntype).to_string(), + kind: K::kind(&dyntype).to_string(), + }; + + let discovery = self.discovery().await?; + let (ar, caps) = discovery.resolve_gvk(&gvk).ok_or_else(|| { + Error::Discovery(DiscoveryError::MissingResource(format!( + "Cannot resolve GVK: {gvk:?}" + ))) + })?; + + let effective_ns = if caps.scope == Scope::Cluster { + None + } else { + namespace.or_else(|| resource.meta().namespace.as_deref()) + }; + + let api: Api = + get_dynamic_api(ar, caps, self.client.clone(), effective_ns, false); + + let name = resource + .meta() + .name + .as_deref() + .expect("Kubernetes resource must have a name"); + + if self.dry_run { + show_dry_run(&api, name, resource).await?; + return Ok(resource.clone()); + } + + let patch_params = PatchParams::apply(FIELD_MANAGER); + do_apply(&api, name, resource, &patch_params, &write_mode) + .await + .and_then(helper::dyn_to_typed) + } + + /// Applies resources in order, one at a time + pub async fn apply_many(&self, resources: &[K], ns: Option<&str>) -> Result, Error> + where + K: Resource + Clone + std::fmt::Debug + DeserializeOwned + Serialize, + ::DynamicType: Default, + { + let mut result = Vec::new(); + for r in resources.iter() { + let res = self.apply(r, ns).await; + if res.is_err() { + // NOTE: this may log sensitive data; downgrade to debug if needed. + warn!( + "Failed to apply k8s resource: {}", + serde_json::to_string_pretty(r).map_err(Error::SerdeError)? + ); + } + result.push(res?); + } + Ok(result) + } + + /// Apply a [`DynamicObject`] resource using server-side apply. + pub async fn apply_dynamic( + &self, + resource: &DynamicObject, + namespace: Option<&str>, + force_conflicts: bool, + ) -> Result { + trace!("apply_dynamic {resource:#?} ns={namespace:?} force={force_conflicts}"); + + let discovery = self.discovery().await?; + let type_meta = resource.types.as_ref().ok_or_else(|| { + Error::BuildRequest(kube::core::request::Error::Validation( + "DynamicObject must have types (apiVersion and kind)".to_string(), + )) + })?; + + let gvk = GroupVersionKind::try_from(type_meta).map_err(|_| { + Error::BuildRequest(kube::core::request::Error::Validation(format!( + "Invalid GVK in DynamicObject: {type_meta:?}" + ))) + })?; + + let (ar, caps) = discovery.resolve_gvk(&gvk).ok_or_else(|| { + Error::Discovery(DiscoveryError::MissingResource(format!( + "Cannot resolve GVK: {gvk:?}" + ))) + })?; + + let effective_ns = if caps.scope == Scope::Cluster { + None + } else { + namespace.or_else(|| resource.metadata.namespace.as_deref()) + }; + + let api = get_dynamic_api(ar, caps, self.client.clone(), effective_ns, false); + let name = resource.metadata.name.as_deref().ok_or_else(|| { + Error::BuildRequest(kube::core::request::Error::Validation( + "DynamicObject must have metadata.name".to_string(), + )) + })?; + + debug!( + "apply_dynamic kind={:?} name='{name}' ns={effective_ns:?}", + resource.types.as_ref().map(|t| &t.kind), + ); + + // NOTE would be nice to improve cohesion between the dynamic and typed apis and avoid copy + // pasting the dry_run and some more logic + if self.dry_run { + show_dry_run(&api, name, resource).await?; + return Ok(resource.clone()); + } + + let mut patch_params = PatchParams::apply(FIELD_MANAGER); + patch_params.force = force_conflicts; + + do_apply( + &api, + name, + resource, + &patch_params, + &WriteMode::CreateOrUpdate, + ) + .await + } + + pub async fn apply_dynamic_many( + &self, + resources: &[DynamicObject], + namespace: Option<&str>, + force_conflicts: bool, + ) -> Result, Error> { + let mut result = Vec::new(); + for r in resources.iter() { + result.push(self.apply_dynamic(r, namespace, force_conflicts).await?); + } + Ok(result) + } + + pub async fn apply_yaml_many( + &self, + #[allow(clippy::ptr_arg)] yaml: &Vec, + ns: Option<&str>, + ) -> Result<(), Error> { + for y in yaml.iter() { + self.apply_yaml(y, ns).await?; + } + Ok(()) + } + + pub async fn apply_yaml( + &self, + yaml: &serde_yaml::Value, + ns: Option<&str>, + ) -> Result<(), Error> { + // NOTE wouldn't it be possible to parse this into a DynamicObject and simply call + // apply_dynamic instead of reimplementing api interactions? + let obj: DynamicObject = + serde_yaml::from_value(yaml.clone()).expect("YAML must deserialise to DynamicObject"); + let name = obj.metadata.name.as_ref().expect("YAML must have a name"); + + let api_version = yaml["apiVersion"].as_str().expect("missing apiVersion"); + let kind = yaml["kind"].as_str().expect("missing kind"); + + let mut it = api_version.splitn(2, '/'); + let first = it.next().unwrap(); + let (g, v) = match it.next() { + Some(second) => (first, second), + None => ("", first), + }; + + let api_resource = ApiResource::from_gvk(&GroupVersionKind::gvk(g, v, kind)); + let namespace = ns.unwrap_or_else(|| { + obj.metadata + .namespace + .as_deref() + .expect("YAML must have a namespace when ns is not provided") + }); + + let api: Api = + Api::namespaced_with(self.client.clone(), namespace, &api_resource); + + println!("Applying '{name}' in namespace '{namespace}'..."); + let patch_params = PatchParams::apply(FIELD_MANAGER); + let result = api.patch(name, &patch_params, &Patch::Apply(&obj)).await?; + println!("Successfully applied '{}'.", result.name_any()); + Ok(()) + } + + /// Equivalent to `kubectl apply -f `. + pub async fn apply_url(&self, url: Url, ns: Option<&str>) -> Result<(), Error> { + let patch_params = PatchParams::apply(FIELD_MANAGER); + let discovery = self.discovery().await?; + + let yaml = reqwest::get(url) + .await + .expect("Could not fetch URL") + .text() + .await + .expect("Could not read response body"); + + for doc in multidoc_deserialize(&yaml).expect("Failed to parse YAML from URL") { + let obj: DynamicObject = + serde_yaml::from_value(doc).expect("YAML document is not a valid object"); + let namespace = obj.metadata.namespace.as_deref().or(ns); + let type_meta = obj.types.as_ref().expect("Object is missing TypeMeta"); + let gvk = + GroupVersionKind::try_from(type_meta).expect("Object has invalid GroupVersionKind"); + let name = obj.name_any(); + + if let Some((ar, caps)) = discovery.resolve_gvk(&gvk) { + let api = get_dynamic_api(ar, caps, self.client.clone(), namespace, false); + trace!( + "Applying {}:\n{}", + gvk.kind, + serde_yaml::to_string(&obj).unwrap_or_default() + ); + let data: Value = serde_json::to_value(&obj).expect("serialisation failed"); + let _r = api.patch(&name, &patch_params, &Patch::Apply(data)).await?; + debug!("Applied {} '{name}'", gvk.kind); + } else { + warn!("Skipping document with unknown GVK: {gvk:?}"); + } + } + Ok(()) + } + + /// Build a dynamic API client from a [`DynamicObject`]'s type metadata. + pub(crate) fn get_api_for_dynamic_object( + &self, + object: &DynamicObject, + ns: Option<&str>, + ) -> Result, Error> { + let ar = object + .types + .as_ref() + .and_then(|t| { + let parts: Vec<&str> = t.api_version.split('/').collect(); + match parts.as_slice() { + [version] => Some(ApiResource::from_gvk(&GroupVersionKind::gvk( + "", version, &t.kind, + ))), + [group, version] => Some(ApiResource::from_gvk(&GroupVersionKind::gvk( + group, version, &t.kind, + ))), + _ => None, + } + }) + .ok_or_else(|| { + Error::BuildRequest(kube::core::request::Error::Validation(format!( + "Invalid apiVersion in DynamicObject: {object:#?}" + ))) + })?; + + Ok(match ns { + Some(ns) => Api::namespaced_with(self.client.clone(), ns, &ar), + None => Api::default_namespaced_with(self.client.clone(), &ar), + }) + } +} + +// ── Free functions ─────────────────────────────────────────────────────────── + +pub(crate) fn get_dynamic_api( + resource: kube::api::ApiResource, + capabilities: kube::discovery::ApiCapabilities, + client: Client, + ns: Option<&str>, + all: bool, +) -> Api { + if capabilities.scope == Scope::Cluster || all { + Api::all_with(client, &resource) + } else if let Some(namespace) = ns { + Api::namespaced_with(client, namespace, &resource) + } else { + Api::default_namespaced_with(client, &resource) + } +} + +pub(crate) fn multidoc_deserialize( + data: &str, +) -> Result, serde_yaml::Error> { + use serde::Deserialize; + let mut docs = vec![]; + for de in serde_yaml::Deserializer::from_str(data) { + docs.push(serde_yaml::Value::deserialize(de)?); + } + Ok(docs) +} + +// ── Tests ──────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod apply_tests { + use std::collections::BTreeMap; + use std::time::{SystemTime, UNIX_EPOCH}; + + use k8s_openapi::api::core::v1::ConfigMap; + use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; + use kube::api::{DeleteParams, TypeMeta}; + + use super::*; + + #[tokio::test] + #[ignore = "requires kubernetes cluster"] + async fn apply_creates_new_configmap() { + let client = K8sClient::try_default().await.unwrap(); + let ns = "default"; + let name = format!( + "test-cm-{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() + ); + + let cm = ConfigMap { + metadata: ObjectMeta { + name: Some(name.clone()), + namespace: Some(ns.to_string()), + ..Default::default() + }, + data: Some(BTreeMap::from([("key1".to_string(), "value1".to_string())])), + ..Default::default() + }; + + assert!(client.apply(&cm, Some(ns)).await.is_ok()); + + let api: Api = Api::namespaced(client.client.clone(), ns); + let _ = api.delete(&name, &DeleteParams::default()).await; + } + + #[tokio::test] + #[ignore = "requires kubernetes cluster"] + async fn apply_is_idempotent() { + let client = K8sClient::try_default().await.unwrap(); + let ns = "default"; + let name = format!( + "test-idem-{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() + ); + + let cm = ConfigMap { + metadata: ObjectMeta { + name: Some(name.clone()), + namespace: Some(ns.to_string()), + ..Default::default() + }, + data: Some(BTreeMap::from([("key".to_string(), "value".to_string())])), + ..Default::default() + }; + + assert!( + client.apply(&cm, Some(ns)).await.is_ok(), + "first apply failed" + ); + assert!( + client.apply(&cm, Some(ns)).await.is_ok(), + "second apply failed (not idempotent)" + ); + + let api: Api = Api::namespaced(client.client.clone(), ns); + let _ = api.delete(&name, &DeleteParams::default()).await; + } + + #[tokio::test] + #[ignore = "requires kubernetes cluster"] + async fn apply_dynamic_creates_new_resource() { + let client = K8sClient::try_default().await.unwrap(); + let ns = "default"; + let name = format!( + "test-dyn-{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() + ); + + let obj = DynamicObject { + types: Some(TypeMeta { + api_version: "v1".to_string(), + kind: "ConfigMap".to_string(), + }), + metadata: ObjectMeta { + name: Some(name.clone()), + namespace: Some(ns.to_string()), + ..Default::default() + }, + data: serde_json::json!({}), + }; + + let result = client.apply_dynamic(&obj, Some(ns), false).await; + assert!(result.is_ok(), "apply_dynamic failed: {:?}", result.err()); + + let api: Api = Api::namespaced(client.client.clone(), ns); + let _ = api.delete(&name, &DeleteParams::default()).await; + } +} diff --git a/harmony/src/domain/topology/k8s/bundle.rs b/harmony-k8s/src/bundle.rs similarity index 96% rename from harmony/src/domain/topology/k8s/bundle.rs rename to harmony-k8s/src/bundle.rs index d826201..0a15341 100644 --- a/harmony/src/domain/topology/k8s/bundle.rs +++ b/harmony-k8s/src/bundle.rs @@ -25,9 +25,9 @@ //! //! ## Example //! -//! ```rust,no_run -//! use harmony::topology::k8s::{K8sClient, helper}; -//! use harmony::topology::KubernetesDistribution; +//! ``` +//! use harmony_k8s::{K8sClient, helper}; +//! use harmony_k8s::KubernetesDistribution; //! //! async fn write_network_config(client: &K8sClient, node: &str) { //! // Create a bundle with platform-specific RBAC @@ -56,7 +56,7 @@ use kube::{Error, Resource, ResourceExt, api::DynamicObject}; use serde::Serialize; use serde_json; -use crate::domain::topology::k8s::K8sClient; +use crate::K8sClient; /// A ResourceBundle represents a logical unit of work consisting of multiple /// Kubernetes resources that should be applied or deleted together. diff --git a/harmony-k8s/src/client.rs b/harmony-k8s/src/client.rs new file mode 100644 index 0000000..9b22602 --- /dev/null +++ b/harmony-k8s/src/client.rs @@ -0,0 +1,99 @@ +use std::sync::Arc; + +use kube::config::{KubeConfigOptions, Kubeconfig}; +use kube::{Client, Config, Discovery, Error}; +use log::error; +use serde::Serialize; +use tokio::sync::OnceCell; + +use crate::types::KubernetesDistribution; + +// TODO not cool, should use a proper configuration mechanism +// cli arg, env var, config file +fn read_dry_run_from_env() -> bool { + std::env::var("DRY_RUN") + .map(|v| v == "true" || v == "1") + .unwrap_or(false) +} + +#[derive(Clone)] +pub struct K8sClient { + pub(crate) client: Client, + /// When `true` no mutation is sent to the API server; diffs are printed + /// to stdout instead. Initialised from the `DRY_RUN` environment variable. + pub(crate) dry_run: bool, + pub(crate) k8s_distribution: Arc>, + pub(crate) discovery: Arc>, +} + +impl Serialize for K8sClient { + fn serialize(&self, _serializer: S) -> Result + where + S: serde::Serializer, + { + todo!("K8sClient serialization is not meaningful; remove this impl if unused") + } +} + +impl std::fmt::Debug for K8sClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "K8sClient {{ namespace: {}, dry_run: {} }}", + self.client.default_namespace(), + self.dry_run, + )) + } +} + +impl K8sClient { + /// Create a client, reading `DRY_RUN` from the environment. + pub fn new(client: Client) -> Self { + Self { + dry_run: read_dry_run_from_env(), + client, + k8s_distribution: Arc::new(OnceCell::new()), + discovery: Arc::new(OnceCell::new()), + } + } + + /// Create a client that always operates in dry-run mode, regardless of + /// the environment variable. + pub fn new_dry_run(client: Client) -> Self { + Self { + dry_run: true, + ..Self::new(client) + } + } + + /// Returns `true` if this client is operating in dry-run mode. + pub fn is_dry_run(&self) -> bool { + self.dry_run + } + + pub async fn try_default() -> Result { + Ok(Self::new(Client::try_default().await?)) + } + + pub async fn from_kubeconfig(path: &str) -> Option { + Self::from_kubeconfig_with_opts(path, &KubeConfigOptions::default()).await + } + + pub async fn from_kubeconfig_with_context(path: &str, context: Option) -> Option { + let mut opts = KubeConfigOptions::default(); + opts.context = context; + Self::from_kubeconfig_with_opts(path, &opts).await + } + + pub async fn from_kubeconfig_with_opts(path: &str, opts: &KubeConfigOptions) -> Option { + let k = match Kubeconfig::read_from(path) { + Ok(k) => k, + Err(e) => { + error!("Failed to load kubeconfig from {path}: {e}"); + return None; + } + }; + Some(Self::new( + Client::try_from(Config::from_custom_kubeconfig(k, opts).await.unwrap()).unwrap(), + )) + } +} diff --git a/harmony/src/domain/topology/k8s/config.rs b/harmony-k8s/src/config.rs similarity index 100% rename from harmony/src/domain/topology/k8s/config.rs rename to harmony-k8s/src/config.rs diff --git a/harmony-k8s/src/discovery.rs b/harmony-k8s/src/discovery.rs new file mode 100644 index 0000000..3acc487 --- /dev/null +++ b/harmony-k8s/src/discovery.rs @@ -0,0 +1,83 @@ +use std::time::Duration; + +use kube::{Discovery, Error}; +use log::{debug, error, info, trace, warn}; +use tokio::sync::Mutex; +use tokio_retry::{Retry, strategy::ExponentialBackoff}; + +use crate::client::K8sClient; +use crate::types::KubernetesDistribution; + +impl K8sClient { + pub async fn get_apiserver_version( + &self, + ) -> Result { + self.client.clone().apiserver_version().await + } + + /// Runs (and caches) Kubernetes API discovery with exponential-backoff retries. + pub async fn discovery(&self) -> Result<&Discovery, Error> { + let retry_strategy = ExponentialBackoff::from_millis(1000) + .max_delay(Duration::from_secs(32)) + .take(6); + + let attempt = Mutex::new(0u32); + Retry::spawn(retry_strategy, || async { + let mut n = attempt.lock().await; + *n += 1; + match self + .discovery + .get_or_try_init(async || { + debug!("Running Kubernetes API discovery (attempt {})", *n); + let d = Discovery::new(self.client.clone()).run().await?; + debug!("Kubernetes API discovery completed"); + Ok(d) + }) + .await + { + Ok(d) => Ok(d), + Err(e) => { + warn!("Kubernetes API discovery failed (attempt {}): {}", *n, e); + Err(e) + } + } + }) + .await + .map_err(|e| { + error!("Kubernetes API discovery failed after all retries: {}", e); + e + }) + } + + /// Detect which Kubernetes distribution is running. Result is cached for + /// the lifetime of the client. + pub async fn get_k8s_distribution(&self) -> Result { + self.k8s_distribution + .get_or_try_init(async || { + debug!("Detecting Kubernetes distribution"); + let api_groups = self.client.list_api_groups().await?; + trace!("list_api_groups: {:?}", api_groups); + + let version = self.get_apiserver_version().await?; + + if api_groups + .groups + .iter() + .any(|g| g.name == "project.openshift.io") + { + info!("Detected distribution: OpenshiftFamily"); + return Ok(KubernetesDistribution::OpenshiftFamily); + } + + if version.git_version.contains("k3s") { + info!("Detected distribution: K3sFamily"); + return Ok(KubernetesDistribution::K3sFamily); + } + + info!("Distribution not identified, using Default"); + Ok(KubernetesDistribution::Default) + }) + .await + .cloned() + } +} diff --git a/harmony/src/domain/topology/k8s/helper.rs b/harmony-k8s/src/helper.rs similarity index 99% rename from harmony/src/domain/topology/k8s/helper.rs rename to harmony-k8s/src/helper.rs index b0917f8..9b64034 100644 --- a/harmony/src/domain/topology/k8s/helper.rs +++ b/harmony-k8s/src/helper.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use std::time::Duration; -use crate::topology::KubernetesDistribution; +use crate::KubernetesDistribution; use super::bundle::ResourceBundle; use super::config::PRIVILEGED_POD_IMAGE; @@ -133,9 +133,9 @@ pub fn host_root_volume() -> (Volume, VolumeMount) { /// /// # Example /// -/// ```rust,no_run -/// # use harmony::topology::k8s::helper::{build_privileged_bundle, PrivilegedPodConfig}; -/// # use harmony::topology::KubernetesDistribution; +/// ``` +/// use harmony_k8s::helper::{build_privileged_bundle, PrivilegedPodConfig}; +/// use harmony_k8s::KubernetesDistribution; /// let bundle = build_privileged_bundle( /// PrivilegedPodConfig { /// name: "network-setup".to_string(), diff --git a/harmony-k8s/src/lib.rs b/harmony-k8s/src/lib.rs new file mode 100644 index 0000000..ec9556b --- /dev/null +++ b/harmony-k8s/src/lib.rs @@ -0,0 +1,13 @@ +pub mod apply; +pub mod bundle; +pub mod client; +pub mod config; +pub mod discovery; +pub mod helper; +pub mod node; +pub mod pod; +pub mod resources; +pub mod types; + +pub use client::K8sClient; +pub use types::{DrainOptions, KubernetesDistribution, NodeFile, ScopeResolver, WriteMode}; diff --git a/harmony-k8s/src/main.rs b/harmony-k8s/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/harmony-k8s/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/harmony-k8s/src/node.rs b/harmony-k8s/src/node.rs new file mode 100644 index 0000000..34f2af7 --- /dev/null +++ b/harmony-k8s/src/node.rs @@ -0,0 +1,722 @@ +use std::collections::BTreeMap; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use k8s_openapi::api::core::v1::{ + ConfigMap, ConfigMapVolumeSource, Node, Pod, Volume, VolumeMount, +}; +use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; +use kube::{ + Error, + api::{Api, DeleteParams, EvictParams, ListParams, PostParams}, + core::ErrorResponse, + error::DiscoveryError, +}; +use log::{debug, error, info, warn}; +use tokio::time::sleep; + +use crate::client::K8sClient; +use crate::helper::{self, PrivilegedPodConfig}; +use crate::types::{DrainOptions, NodeFile}; + +impl K8sClient { + pub async fn cordon_node(&self, node_name: &str) -> Result<(), Error> { + Api::::all(self.client.clone()) + .cordon(node_name) + .await?; + Ok(()) + } + + pub async fn uncordon_node(&self, node_name: &str) -> Result<(), Error> { + Api::::all(self.client.clone()) + .uncordon(node_name) + .await?; + Ok(()) + } + + pub async fn wait_for_node_ready(&self, node_name: &str) -> Result<(), Error> { + self.wait_for_node_ready_with_timeout(node_name, Duration::from_secs(600)) + .await + } + + async fn wait_for_node_ready_with_timeout( + &self, + node_name: &str, + timeout: Duration, + ) -> Result<(), Error> { + let api: Api = Api::all(self.client.clone()); + let start = tokio::time::Instant::now(); + let poll = Duration::from_secs(5); + loop { + if start.elapsed() > timeout { + return Err(Error::Discovery(DiscoveryError::MissingResource(format!( + "Node '{node_name}' did not become Ready within {timeout:?}" + )))); + } + match api.get(node_name).await { + Ok(node) => { + if node + .status + .as_ref() + .and_then(|s| s.conditions.as_ref()) + .map(|conds| { + conds + .iter() + .any(|c| c.type_ == "Ready" && c.status == "True") + }) + .unwrap_or(false) + { + debug!("Node '{node_name}' is Ready"); + return Ok(()); + } + } + Err(e) => debug!("Error polling node '{node_name}': {e}"), + } + sleep(poll).await; + } + } + + async fn wait_for_node_not_ready( + &self, + node_name: &str, + timeout: Duration, + ) -> Result<(), Error> { + let api: Api = Api::all(self.client.clone()); + let start = tokio::time::Instant::now(); + let poll = Duration::from_secs(5); + loop { + if start.elapsed() > timeout { + return Err(Error::Discovery(DiscoveryError::MissingResource(format!( + "Node '{node_name}' did not become NotReady within {timeout:?}" + )))); + } + match api.get(node_name).await { + Ok(node) => { + let is_ready = node + .status + .as_ref() + .and_then(|s| s.conditions.as_ref()) + .map(|conds| { + conds + .iter() + .any(|c| c.type_ == "Ready" && c.status == "True") + }) + .unwrap_or(false); + if !is_ready { + debug!("Node '{node_name}' is NotReady"); + return Ok(()); + } + } + Err(e) => debug!("Error polling node '{node_name}': {e}"), + } + sleep(poll).await; + } + } + + async fn list_pods_on_node(&self, node_name: &str) -> Result, Error> { + let api: Api = Api::all(self.client.clone()); + Ok(api + .list(&ListParams::default().fields(&format!("spec.nodeName={node_name}"))) + .await? + .items) + } + + fn is_mirror_pod(pod: &Pod) -> bool { + pod.metadata + .annotations + .as_ref() + .map(|a| a.contains_key("kubernetes.io/config.mirror")) + .unwrap_or(false) + } + + fn is_daemonset_pod(pod: &Pod) -> bool { + pod.metadata + .owner_references + .as_ref() + .map(|refs| refs.iter().any(|r| r.kind == "DaemonSet")) + .unwrap_or(false) + } + + fn has_emptydir_volume(pod: &Pod) -> bool { + pod.spec + .as_ref() + .and_then(|s| s.volumes.as_ref()) + .map(|vols| vols.iter().any(|v| v.empty_dir.is_some())) + .unwrap_or(false) + } + + fn is_completed_pod(pod: &Pod) -> bool { + pod.status + .as_ref() + .and_then(|s| s.phase.as_deref()) + .map(|phase| phase == "Succeeded" || phase == "Failed") + .unwrap_or(false) + } + + fn classify_pods_for_drain( + pods: &[Pod], + options: &DrainOptions, + ) -> Result<(Vec, Vec), String> { + let mut evictable = Vec::new(); + let mut skipped = Vec::new(); + let mut blocking = Vec::new(); + + for pod in pods { + let name = pod.metadata.name.as_deref().unwrap_or(""); + let ns = pod.metadata.namespace.as_deref().unwrap_or(""); + let qualified = format!("{ns}/{name}"); + + if Self::is_mirror_pod(pod) { + skipped.push(format!("{qualified} (mirror pod)")); + continue; + } + if Self::is_completed_pod(pod) { + skipped.push(format!("{qualified} (completed)")); + continue; + } + if Self::is_daemonset_pod(pod) { + if options.ignore_daemonsets { + skipped.push(format!("{qualified} (DaemonSet-managed)")); + } else { + blocking.push(format!( + "{qualified} is managed by a DaemonSet (set ignore_daemonsets to skip)" + )); + } + continue; + } + if Self::has_emptydir_volume(pod) && !options.delete_emptydir_data { + blocking.push(format!( + "{qualified} uses emptyDir volumes (set delete_emptydir_data to allow eviction)" + )); + continue; + } + evictable.push(pod.clone()); + } + + if !blocking.is_empty() { + return Err(format!( + "Cannot drain node — the following pods block eviction:\n - {}", + blocking.join("\n - ") + )); + } + Ok((evictable, skipped)) + } + + async fn evict_pod(&self, pod: &Pod) -> Result<(), Error> { + let name = pod.metadata.name.as_deref().unwrap_or_default(); + let ns = pod.metadata.namespace.as_deref().unwrap_or_default(); + debug!("Evicting pod {ns}/{name}"); + Api::::namespaced(self.client.clone(), ns) + .evict(name, &EvictParams::default()) + .await + .map(|_| ()) + } + + /// Drains a node: cordon → classify → evict & wait. + pub async fn drain_node(&self, node_name: &str, options: &DrainOptions) -> Result<(), Error> { + debug!("Cordoning '{node_name}'"); + self.cordon_node(node_name).await?; + + let pods = self.list_pods_on_node(node_name).await?; + debug!("Found {} pod(s) on '{node_name}'", pods.len()); + + let (evictable, skipped) = + Self::classify_pods_for_drain(&pods, options).map_err(|msg| { + error!("{msg}"); + Error::Discovery(DiscoveryError::MissingResource(msg)) + })?; + + for s in &skipped { + info!("Skipping pod: {s}"); + } + if evictable.is_empty() { + info!("No pods to evict on '{node_name}'"); + return Ok(()); + } + info!("Evicting {} pod(s) from '{node_name}'", evictable.len()); + + let mut start = tokio::time::Instant::now(); + let poll = Duration::from_secs(5); + let mut pending = evictable; + + loop { + for pod in &pending { + match self.evict_pod(pod).await { + Ok(()) => {} + Err(Error::Api(ErrorResponse { code: 404, .. })) => {} + Err(Error::Api(ErrorResponse { code: 429, .. })) => { + warn!( + "PDB blocked eviction of {}/{}; will retry", + pod.metadata.namespace.as_deref().unwrap_or(""), + pod.metadata.name.as_deref().unwrap_or("") + ); + } + Err(e) => { + error!( + "Failed to evict {}/{}: {e}", + pod.metadata.namespace.as_deref().unwrap_or(""), + pod.metadata.name.as_deref().unwrap_or("") + ); + return Err(e); + } + } + } + + sleep(poll).await; + + let mut still_present = Vec::new(); + for pod in pending { + let ns = pod.metadata.namespace.as_deref().unwrap_or_default(); + let name = pod.metadata.name.as_deref().unwrap_or_default(); + match self.get_pod(name, Some(ns)).await? { + Some(_) => still_present.push(pod), + None => debug!("Pod {ns}/{name} evicted"), + } + } + pending = still_present; + + if pending.is_empty() { + break; + } + + if start.elapsed() > options.timeout { + match helper::prompt_drain_timeout_action( + node_name, + pending.len(), + options.timeout, + )? { + helper::DrainTimeoutAction::Accept => break, + helper::DrainTimeoutAction::Retry => { + start = tokio::time::Instant::now(); + continue; + } + helper::DrainTimeoutAction::Abort => { + return Err(Error::Discovery(DiscoveryError::MissingResource(format!( + "Drain aborted. {} pod(s) remaining on '{node_name}'", + pending.len() + )))); + } + } + } + debug!("Waiting for {} pod(s) on '{node_name}'", pending.len()); + } + + debug!("'{node_name}' drained successfully"); + Ok(()) + } + + /// Safely reboots a node: drain → reboot → wait for Ready → uncordon. + pub async fn reboot_node( + &self, + node_name: &str, + drain_options: &DrainOptions, + timeout: Duration, + ) -> Result<(), Error> { + info!("Starting reboot for '{node_name}'"); + let node_api: Api = Api::all(self.client.clone()); + + let boot_id_before = node_api + .get(node_name) + .await? + .status + .as_ref() + .and_then(|s| s.node_info.as_ref()) + .map(|ni| ni.boot_id.clone()) + .ok_or_else(|| { + Error::Discovery(DiscoveryError::MissingResource(format!( + "Node '{node_name}' has no boot_id in status" + ))) + })?; + + info!("Draining '{node_name}'"); + self.drain_node(node_name, drain_options).await?; + + let start = tokio::time::Instant::now(); + + info!("Scheduling reboot for '{node_name}'"); + let reboot_cmd = + "echo rebooting ; nohup bash -c 'sleep 5 && nsenter -t 1 -m -- systemctl reboot'"; + match self + .run_privileged_command_on_node(node_name, reboot_cmd) + .await + { + Ok(_) => debug!("Reboot command dispatched"), + Err(e) => debug!("Reboot command error (expected if node began shutdown): {e}"), + } + + info!("Waiting for '{node_name}' to begin shutdown"); + self.wait_for_node_not_ready(node_name, timeout.saturating_sub(start.elapsed())) + .await?; + + if start.elapsed() > timeout { + return Err(Error::Discovery(DiscoveryError::MissingResource(format!( + "Timeout during reboot of '{node_name}' (shutdown phase)" + )))); + } + + info!("Waiting for '{node_name}' to come back online"); + self.wait_for_node_ready_with_timeout(node_name, timeout.saturating_sub(start.elapsed())) + .await?; + + if start.elapsed() > timeout { + return Err(Error::Discovery(DiscoveryError::MissingResource(format!( + "Timeout during reboot of '{node_name}' (ready phase)" + )))); + } + + let boot_id_after = node_api + .get(node_name) + .await? + .status + .as_ref() + .and_then(|s| s.node_info.as_ref()) + .map(|ni| ni.boot_id.clone()) + .ok_or_else(|| { + Error::Discovery(DiscoveryError::MissingResource(format!( + "Node '{node_name}' has no boot_id after reboot" + ))) + })?; + + if boot_id_before == boot_id_after { + return Err(Error::Discovery(DiscoveryError::MissingResource(format!( + "Node '{node_name}' did not actually reboot (boot_id unchanged: {boot_id_before})" + )))); + } + + info!("'{node_name}' rebooted ({boot_id_before} → {boot_id_after})"); + self.uncordon_node(node_name).await?; + info!("'{node_name}' reboot complete ({:?})", start.elapsed()); + Ok(()) + } + + /// Write a set of files to a node's filesystem via a privileged ephemeral pod. + pub async fn write_files_to_node( + &self, + node_name: &str, + files: &[NodeFile], + ) -> Result { + let ns = self.client.default_namespace(); + let suffix = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(); + let name = format!("harmony-k8s-writer-{suffix}"); + + debug!("Writing {} file(s) to '{node_name}'", files.len()); + + let mut data = BTreeMap::new(); + let mut script = String::from("set -e\n"); + for (i, file) in files.iter().enumerate() { + let key = format!("f{i}"); + data.insert(key.clone(), file.content.clone()); + script.push_str(&format!("mkdir -p \"$(dirname \"/host{}\")\"\n", file.path)); + script.push_str(&format!("cp \"/payload/{key}\" \"/host{}\"\n", file.path)); + script.push_str(&format!("chmod {:o} \"/host{}\"\n", file.mode, file.path)); + } + + let cm = ConfigMap { + metadata: ObjectMeta { + name: Some(name.clone()), + namespace: Some(ns.to_string()), + ..Default::default() + }, + data: Some(data), + ..Default::default() + }; + + let cm_api: Api = Api::namespaced(self.client.clone(), ns); + cm_api.create(&PostParams::default(), &cm).await?; + debug!("Created ConfigMap '{name}'"); + + let (host_vol, host_mount) = helper::host_root_volume(); + let payload_vol = Volume { + name: "payload".to_string(), + config_map: Some(ConfigMapVolumeSource { + name: name.clone(), + ..Default::default() + }), + ..Default::default() + }; + let payload_mount = VolumeMount { + name: "payload".to_string(), + mount_path: "/payload".to_string(), + ..Default::default() + }; + + let bundle = helper::build_privileged_bundle( + PrivilegedPodConfig { + name: name.clone(), + namespace: ns.to_string(), + node_name: node_name.to_string(), + container_name: "writer".to_string(), + command: vec!["/bin/bash".to_string(), "-c".to_string(), script], + volumes: vec![payload_vol, host_vol], + volume_mounts: vec![payload_mount, host_mount], + host_pid: false, + host_network: false, + }, + &self.get_k8s_distribution().await?, + ); + + bundle.apply(self).await?; + debug!("Created privileged pod bundle '{name}'"); + + let result = self.wait_for_pod_completion(&name, ns).await; + + debug!("Cleaning up '{name}'"); + let _ = bundle.delete(self).await; + let _ = cm_api.delete(&name, &DeleteParams::default()).await; + + result + } + + /// Run a privileged command on a node via an ephemeral pod. + pub async fn run_privileged_command_on_node( + &self, + node_name: &str, + command: &str, + ) -> Result { + let namespace = self.client.default_namespace(); + let suffix = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(); + let name = format!("harmony-k8s-cmd-{suffix}"); + + debug!("Running privileged command on '{node_name}': {command}"); + + let (host_vol, host_mount) = helper::host_root_volume(); + let bundle = helper::build_privileged_bundle( + PrivilegedPodConfig { + name: name.clone(), + namespace: namespace.to_string(), + node_name: node_name.to_string(), + container_name: "runner".to_string(), + command: vec![ + "/bin/bash".to_string(), + "-c".to_string(), + command.to_string(), + ], + volumes: vec![host_vol], + volume_mounts: vec![host_mount], + host_pid: true, + host_network: true, + }, + &self.get_k8s_distribution().await?, + ); + + bundle.apply(self).await?; + debug!("Privileged pod '{name}' created"); + + let result = self.wait_for_pod_completion(&name, namespace).await; + + debug!("Cleaning up '{name}'"); + let _ = bundle.delete(self).await; + + result + } +} + +// ── Tests ──────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use k8s_openapi::api::core::v1::{EmptyDirVolumeSource, PodSpec, PodStatus, Volume}; + use k8s_openapi::apimachinery::pkg::apis::meta::v1::{ObjectMeta, OwnerReference}; + + use super::*; + + fn base_pod(name: &str, ns: &str) -> Pod { + Pod { + metadata: ObjectMeta { + name: Some(name.to_string()), + namespace: Some(ns.to_string()), + ..Default::default() + }, + spec: Some(PodSpec::default()), + status: Some(PodStatus { + phase: Some("Running".to_string()), + ..Default::default() + }), + } + } + + fn mirror_pod(name: &str, ns: &str) -> Pod { + let mut pod = base_pod(name, ns); + pod.metadata.annotations = Some(std::collections::BTreeMap::from([( + "kubernetes.io/config.mirror".to_string(), + "abc123".to_string(), + )])); + pod + } + + fn daemonset_pod(name: &str, ns: &str) -> Pod { + let mut pod = base_pod(name, ns); + pod.metadata.owner_references = Some(vec![OwnerReference { + api_version: "apps/v1".to_string(), + kind: "DaemonSet".to_string(), + name: "some-ds".to_string(), + uid: "uid-ds".to_string(), + ..Default::default() + }]); + pod + } + + fn emptydir_pod(name: &str, ns: &str) -> Pod { + let mut pod = base_pod(name, ns); + pod.spec = Some(PodSpec { + volumes: Some(vec![Volume { + name: "scratch".to_string(), + empty_dir: Some(EmptyDirVolumeSource::default()), + ..Default::default() + }]), + ..Default::default() + }); + pod + } + + fn completed_pod(name: &str, ns: &str, phase: &str) -> Pod { + let mut pod = base_pod(name, ns); + pod.status = Some(PodStatus { + phase: Some(phase.to_string()), + ..Default::default() + }); + pod + } + + fn default_opts() -> DrainOptions { + DrainOptions::default() + } + + // All test bodies are identical to the original — only the module path changed. + + #[test] + fn empty_pod_list_returns_empty_vecs() { + let (e, s) = K8sClient::classify_pods_for_drain(&[], &default_opts()).unwrap(); + assert!(e.is_empty()); + assert!(s.is_empty()); + } + + #[test] + fn normal_pod_is_evictable() { + let pods = vec![base_pod("web", "default")]; + let (e, s) = K8sClient::classify_pods_for_drain(&pods, &default_opts()).unwrap(); + assert_eq!(e.len(), 1); + assert!(s.is_empty()); + } + + #[test] + fn mirror_pod_is_skipped() { + let pods = vec![mirror_pod("kube-apiserver", "kube-system")]; + let (e, s) = K8sClient::classify_pods_for_drain(&pods, &default_opts()).unwrap(); + assert!(e.is_empty()); + assert!(s[0].contains("mirror pod")); + } + + #[test] + fn completed_pods_are_skipped() { + for phase in ["Succeeded", "Failed"] { + let pods = vec![completed_pod("job", "batch", phase)]; + let (e, s) = K8sClient::classify_pods_for_drain(&pods, &default_opts()).unwrap(); + assert!(e.is_empty()); + assert!(s[0].contains("completed")); + } + } + + #[test] + fn daemonset_skipped_when_ignored() { + let pods = vec![daemonset_pod("fluentd", "logging")]; + let opts = DrainOptions { + ignore_daemonsets: true, + ..default_opts() + }; + let (e, s) = K8sClient::classify_pods_for_drain(&pods, &opts).unwrap(); + assert!(e.is_empty()); + assert!(s[0].contains("DaemonSet-managed")); + } + + #[test] + fn daemonset_blocks_when_not_ignored() { + let pods = vec![daemonset_pod("fluentd", "logging")]; + let opts = DrainOptions { + ignore_daemonsets: false, + ..default_opts() + }; + let err = K8sClient::classify_pods_for_drain(&pods, &opts).unwrap_err(); + assert!(err.contains("DaemonSet") && err.contains("logging/fluentd")); + } + + #[test] + fn emptydir_blocks_without_flag() { + let pods = vec![emptydir_pod("cache", "default")]; + let opts = DrainOptions { + delete_emptydir_data: false, + ..default_opts() + }; + let err = K8sClient::classify_pods_for_drain(&pods, &opts).unwrap_err(); + assert!(err.contains("emptyDir") && err.contains("default/cache")); + } + + #[test] + fn emptydir_evictable_with_flag() { + let pods = vec![emptydir_pod("cache", "default")]; + let opts = DrainOptions { + delete_emptydir_data: true, + ..default_opts() + }; + let (e, s) = K8sClient::classify_pods_for_drain(&pods, &opts).unwrap(); + assert_eq!(e.len(), 1); + assert!(s.is_empty()); + } + + #[test] + fn multiple_blocking_all_reported() { + let pods = vec![daemonset_pod("ds", "ns1"), emptydir_pod("ed", "ns2")]; + let opts = DrainOptions { + ignore_daemonsets: false, + delete_emptydir_data: false, + ..default_opts() + }; + let err = K8sClient::classify_pods_for_drain(&pods, &opts).unwrap_err(); + assert!(err.contains("ns1/ds") && err.contains("ns2/ed")); + } + + #[test] + fn mixed_pods_classified_correctly() { + let pods = vec![ + base_pod("web", "default"), + mirror_pod("kube-apiserver", "kube-system"), + daemonset_pod("fluentd", "logging"), + completed_pod("job", "batch", "Succeeded"), + base_pod("api", "default"), + ]; + let (e, s) = K8sClient::classify_pods_for_drain(&pods, &default_opts()).unwrap(); + let names: Vec<&str> = e + .iter() + .map(|p| p.metadata.name.as_deref().unwrap()) + .collect(); + assert_eq!(names, vec!["web", "api"]); + assert_eq!(s.len(), 3); + } + + #[test] + fn mirror_checked_before_completed() { + let mut pod = mirror_pod("static-etcd", "kube-system"); + pod.status = Some(PodStatus { + phase: Some("Succeeded".to_string()), + ..Default::default() + }); + let (_, s) = K8sClient::classify_pods_for_drain(&[pod], &default_opts()).unwrap(); + assert!(s[0].contains("mirror pod"), "got: {}", s[0]); + } + + #[test] + fn completed_checked_before_daemonset() { + let mut pod = daemonset_pod("collector", "monitoring"); + pod.status = Some(PodStatus { + phase: Some("Failed".to_string()), + ..Default::default() + }); + let (_, s) = K8sClient::classify_pods_for_drain(&[pod], &default_opts()).unwrap(); + assert!(s[0].contains("completed"), "got: {}", s[0]); + } +} diff --git a/harmony-k8s/src/pod.rs b/harmony-k8s/src/pod.rs new file mode 100644 index 0000000..3c1efbd --- /dev/null +++ b/harmony-k8s/src/pod.rs @@ -0,0 +1,193 @@ +use std::time::Duration; + +use k8s_openapi::api::core::v1::Pod; +use kube::{ + Error, + api::{Api, AttachParams, ListParams}, + error::DiscoveryError, + runtime::reflector::Lookup, +}; +use log::debug; +use tokio::io::AsyncReadExt; +use tokio::time::sleep; + +use crate::client::K8sClient; + +impl K8sClient { + pub async fn get_pod(&self, name: &str, namespace: Option<&str>) -> Result, Error> { + let api: Api = match namespace { + Some(ns) => Api::namespaced(self.client.clone(), ns), + None => Api::default_namespaced(self.client.clone()), + }; + api.get_opt(name).await + } + + pub async fn wait_for_pod_ready( + &self, + pod_name: &str, + namespace: Option<&str>, + ) -> Result<(), Error> { + let mut elapsed = 0u64; + let interval = 5u64; + let timeout_secs = 120u64; + loop { + if let Some(p) = self.get_pod(pod_name, namespace).await? { + if let Some(phase) = p.status.and_then(|s| s.phase) { + if phase.to_lowercase() == "running" { + return Ok(()); + } + } + } + if elapsed >= timeout_secs { + return Err(Error::Discovery(DiscoveryError::MissingResource(format!( + "Pod '{}' in '{}' did not become ready within {timeout_secs}s", + pod_name, + namespace.unwrap_or(""), + )))); + } + sleep(Duration::from_secs(interval)).await; + elapsed += interval; + } + } + + /// Polls a pod until it reaches `Succeeded` or `Failed`, then returns its + /// logs. Used internally by node operations. + pub(crate) async fn wait_for_pod_completion( + &self, + name: &str, + namespace: &str, + ) -> Result { + let api: Api = Api::namespaced(self.client.clone(), namespace); + let poll_interval = Duration::from_secs(2); + for _ in 0..60 { + sleep(poll_interval).await; + let p = api.get(name).await?; + match p.status.and_then(|s| s.phase).as_deref() { + Some("Succeeded") => { + let logs = api + .logs(name, &Default::default()) + .await + .unwrap_or_default(); + debug!("Pod {namespace}/{name} succeeded. Logs: {logs}"); + return Ok(logs); + } + Some("Failed") => { + let logs = api + .logs(name, &Default::default()) + .await + .unwrap_or_default(); + debug!("Pod {namespace}/{name} failed. Logs: {logs}"); + return Err(Error::Discovery(DiscoveryError::MissingResource(format!( + "Pod '{name}' failed.\n{logs}" + )))); + } + _ => {} + } + } + Err(Error::Discovery(DiscoveryError::MissingResource(format!( + "Timed out waiting for pod '{name}'" + )))) + } + + /// Execute a command in the first pod matching `{label}={name}`. + pub async fn exec_app_capture_output( + &self, + name: String, + label: String, + namespace: Option<&str>, + command: Vec<&str>, + ) -> Result { + let api: Api = match namespace { + Some(ns) => Api::namespaced(self.client.clone(), ns), + None => Api::default_namespaced(self.client.clone()), + }; + let pod_list = api + .list(&ListParams::default().labels(&format!("{label}={name}"))) + .await + .expect("Failed to list pods"); + + let pod_name = pod_list + .items + .first() + .expect("No matching pod") + .name() + .expect("Pod has no name") + .into_owned(); + + match api + .exec( + &pod_name, + command, + &AttachParams::default().stdout(true).stderr(true), + ) + .await + { + Err(e) => Err(e.to_string()), + Ok(mut process) => { + let status = process + .take_status() + .expect("No status handle") + .await + .expect("Status channel closed"); + + if let Some(s) = status.status { + let mut buf = String::new(); + if let Some(mut stdout) = process.stdout() { + stdout + .read_to_string(&mut buf) + .await + .map_err(|e| format!("Failed to read stdout: {e}"))?; + } + debug!("exec status: {} - {:?}", s, status.details); + if s == "Success" { Ok(buf) } else { Err(s) } + } else { + Err("No inner status from pod exec".to_string()) + } + } + } + } + + /// Execute a command in the first pod matching + /// `app.kubernetes.io/name={name}`. + pub async fn exec_app( + &self, + name: String, + namespace: Option<&str>, + command: Vec<&str>, + ) -> Result<(), String> { + let api: Api = match namespace { + Some(ns) => Api::namespaced(self.client.clone(), ns), + None => Api::default_namespaced(self.client.clone()), + }; + let pod_list = api + .list(&ListParams::default().labels(&format!("app.kubernetes.io/name={name}"))) + .await + .expect("Failed to list pods"); + + let pod_name = pod_list + .items + .first() + .expect("No matching pod") + .name() + .expect("Pod has no name") + .into_owned(); + + match api.exec(&pod_name, command, &AttachParams::default()).await { + Err(e) => Err(e.to_string()), + Ok(mut process) => { + let status = process + .take_status() + .expect("No status handle") + .await + .expect("Status channel closed"); + + if let Some(s) = status.status { + debug!("exec status: {} - {:?}", s, status.details); + if s == "Success" { Ok(()) } else { Err(s) } + } else { + Err("No inner status from pod exec".to_string()) + } + } + } + } +} diff --git a/harmony-k8s/src/resources.rs b/harmony-k8s/src/resources.rs new file mode 100644 index 0000000..b788318 --- /dev/null +++ b/harmony-k8s/src/resources.rs @@ -0,0 +1,316 @@ +use std::collections::HashMap; + +use k8s_openapi::api::{ + apps::v1::Deployment, + core::v1::{Node, ServiceAccount}, +}; +use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition; +use kube::api::ApiResource; +use kube::{ + Error, Resource, + api::{Api, DynamicObject, GroupVersionKind, ListParams, ObjectList}, + runtime::conditions, + runtime::wait::await_condition, +}; +use log::debug; +use serde::de::DeserializeOwned; +use serde_json::Value; +use std::time::Duration; + +use crate::client::K8sClient; +use crate::types::ScopeResolver; + +impl K8sClient { + pub async fn has_healthy_deployment_with_label( + &self, + namespace: &str, + label_selector: &str, + ) -> Result { + let api: Api = Api::namespaced(self.client.clone(), namespace); + let list = api + .list(&ListParams::default().labels(label_selector)) + .await?; + for d in list.items { + let available = d + .status + .as_ref() + .and_then(|s| s.available_replicas) + .unwrap_or(0); + if available > 0 { + return Ok(true); + } + if let Some(conds) = d.status.as_ref().and_then(|s| s.conditions.as_ref()) { + if conds + .iter() + .any(|c| c.type_ == "Available" && c.status == "True") + { + return Ok(true); + } + } + } + Ok(false) + } + + pub async fn list_namespaces_with_healthy_deployments( + &self, + label_selector: &str, + ) -> Result, Error> { + let api: Api = Api::all(self.client.clone()); + let list = api + .list(&ListParams::default().labels(label_selector)) + .await?; + + let mut healthy_ns: HashMap = HashMap::new(); + for d in list.items { + let ns = match d.metadata.namespace.clone() { + Some(n) => n, + None => continue, + }; + let available = d + .status + .as_ref() + .and_then(|s| s.available_replicas) + .unwrap_or(0); + let is_healthy = if available > 0 { + true + } else { + d.status + .as_ref() + .and_then(|s| s.conditions.as_ref()) + .map(|c| { + c.iter() + .any(|c| c.type_ == "Available" && c.status == "True") + }) + .unwrap_or(false) + }; + if is_healthy { + healthy_ns.insert(ns, true); + } + } + Ok(healthy_ns.into_keys().collect()) + } + + pub async fn get_controller_service_account_name( + &self, + ns: &str, + ) -> Result, Error> { + let api: Api = Api::namespaced(self.client.clone(), ns); + let list = api + .list(&ListParams::default().labels("app.kubernetes.io/component=controller")) + .await?; + if let Some(dep) = list.items.first() { + if let Some(sa) = dep + .spec + .as_ref() + .and_then(|s| s.template.spec.as_ref()) + .and_then(|s| s.service_account_name.clone()) + { + return Ok(Some(sa)); + } + } + Ok(None) + } + + pub async fn list_clusterrolebindings_json(&self) -> Result, Error> { + let gvk = GroupVersionKind::gvk("rbac.authorization.k8s.io", "v1", "ClusterRoleBinding"); + let ar = ApiResource::from_gvk(&gvk); + let api: Api = Api::all_with(self.client.clone(), &ar); + let list = api.list(&ListParams::default()).await?; + Ok(list + .items + .into_iter() + .map(|o| serde_json::to_value(&o).unwrap_or(Value::Null)) + .collect()) + } + + pub async fn is_service_account_cluster_wide(&self, sa: &str, ns: &str) -> Result { + let sa_user = format!("system:serviceaccount:{ns}:{sa}"); + for crb in self.list_clusterrolebindings_json().await? { + if let Some(subjects) = crb.get("subjects").and_then(|s| s.as_array()) { + for subj in subjects { + let kind = subj.get("kind").and_then(|v| v.as_str()).unwrap_or(""); + let name = subj.get("name").and_then(|v| v.as_str()).unwrap_or(""); + let subj_ns = subj.get("namespace").and_then(|v| v.as_str()).unwrap_or(""); + if (kind == "ServiceAccount" && name == sa && subj_ns == ns) + || (kind == "User" && name == sa_user) + { + return Ok(true); + } + } + } + } + Ok(false) + } + + pub async fn has_crd(&self, name: &str) -> Result { + let api: Api = Api::all(self.client.clone()); + let crds = api + .list(&ListParams::default().fields(&format!("metadata.name={name}"))) + .await?; + Ok(!crds.items.is_empty()) + } + + pub async fn service_account_api(&self, namespace: &str) -> Api { + Api::namespaced(self.client.clone(), namespace) + } + + pub async fn get_resource_json_value( + &self, + name: &str, + namespace: Option<&str>, + gvk: &GroupVersionKind, + ) -> Result { + let ar = ApiResource::from_gvk(gvk); + let api: Api = match namespace { + Some(ns) => Api::namespaced_with(self.client.clone(), ns, &ar), + None => Api::default_namespaced_with(self.client.clone(), &ar), + }; + api.get(name).await + } + + pub async fn get_secret_json_value( + &self, + name: &str, + namespace: Option<&str>, + ) -> Result { + self.get_resource_json_value( + name, + namespace, + &GroupVersionKind { + group: String::new(), + version: "v1".to_string(), + kind: "Secret".to_string(), + }, + ) + .await + } + + pub async fn get_deployment( + &self, + name: &str, + namespace: Option<&str>, + ) -> Result, Error> { + let api: Api = match namespace { + Some(ns) => { + debug!("Getting namespaced deployment '{name}' in '{ns}'"); + Api::namespaced(self.client.clone(), ns) + } + None => { + debug!("Getting deployment '{name}' in default namespace"); + Api::default_namespaced(self.client.clone()) + } + }; + api.get_opt(name).await + } + + pub async fn scale_deployment( + &self, + name: &str, + namespace: Option<&str>, + replicas: u32, + ) -> Result<(), Error> { + let api: Api = match namespace { + Some(ns) => Api::namespaced(self.client.clone(), ns), + None => Api::default_namespaced(self.client.clone()), + }; + use kube::api::{Patch, PatchParams}; + use serde_json::json; + let patch = json!({ "spec": { "replicas": replicas } }); + api.patch_scale(name, &PatchParams::default(), &Patch::Merge(&patch)) + .await?; + Ok(()) + } + + pub async fn delete_deployment( + &self, + name: &str, + namespace: Option<&str>, + ) -> Result<(), Error> { + let api: Api = match namespace { + Some(ns) => Api::namespaced(self.client.clone(), ns), + None => Api::default_namespaced(self.client.clone()), + }; + api.delete(name, &kube::api::DeleteParams::default()) + .await?; + Ok(()) + } + + pub async fn wait_until_deployment_ready( + &self, + name: &str, + namespace: Option<&str>, + timeout: Option, + ) -> Result<(), String> { + let api: Api = match namespace { + Some(ns) => Api::namespaced(self.client.clone(), ns), + None => Api::default_namespaced(self.client.clone()), + }; + let timeout = timeout.unwrap_or(Duration::from_secs(120)); + let establish = await_condition(api, name, conditions::is_deployment_completed()); + tokio::time::timeout(timeout, establish) + .await + .map(|_| ()) + .map_err(|_| "Timed out waiting for deployment".to_string()) + } + + /// Gets a single named resource, using the correct API scope for `K`. + pub async fn get_resource( + &self, + name: &str, + namespace: Option<&str>, + ) -> Result, Error> + where + K: Resource + Clone + std::fmt::Debug + DeserializeOwned, + ::Scope: ScopeResolver, + ::DynamicType: Default, + { + let api: Api = + <::Scope as ScopeResolver>::get_api(&self.client, namespace); + api.get_opt(name).await + } + + pub async fn list_resources( + &self, + namespace: Option<&str>, + list_params: Option, + ) -> Result, Error> + where + K: Resource + Clone + std::fmt::Debug + DeserializeOwned, + ::Scope: ScopeResolver, + ::DynamicType: Default, + { + let api: Api = + <::Scope as ScopeResolver>::get_api(&self.client, namespace); + api.list(&list_params.unwrap_or_default()).await + } + + pub async fn list_all_resources_with_labels(&self, labels: &str) -> Result, Error> + where + K: Resource + Clone + std::fmt::Debug + DeserializeOwned, + ::DynamicType: Default, + { + Api::::all(self.client.clone()) + .list(&ListParams::default().labels(labels)) + .await + .map(|l| l.items) + } + + pub async fn get_all_resource_in_all_namespace(&self) -> Result, Error> + where + K: Resource + Clone + std::fmt::Debug + DeserializeOwned, + ::Scope: ScopeResolver, + ::DynamicType: Default, + { + Api::::all(self.client.clone()) + .list(&Default::default()) + .await + .map(|l| l.items) + } + + pub async fn get_nodes( + &self, + list_params: Option, + ) -> Result, Error> { + self.list_resources(None, list_params).await + } +} diff --git a/harmony-k8s/src/types.rs b/harmony-k8s/src/types.rs new file mode 100644 index 0000000..8535331 --- /dev/null +++ b/harmony-k8s/src/types.rs @@ -0,0 +1,100 @@ +use std::time::Duration; + +use k8s_openapi::{ClusterResourceScope, NamespaceResourceScope}; +use kube::{Api, Client, Resource}; +use serde::Serialize; + +/// Which Kubernetes distribution is running. Detected once at runtime via +/// [`crate::discovery::K8sClient::get_k8s_distribution`]. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub enum KubernetesDistribution { + Default, + OpenshiftFamily, + K3sFamily, +} + +/// A file to be written to a node's filesystem. +#[derive(Debug, Clone)] +pub struct NodeFile { + /// Absolute path on the host where the file should be written. + pub path: String, + /// Content of the file. + pub content: String, + /// UNIX permissions (e.g. `0o600`). + pub mode: u32, +} + +/// Options controlling the behaviour of a [`crate::K8sClient::drain_node`] operation. +#[derive(Debug, Clone)] +pub struct DrainOptions { + /// Evict pods that use `emptyDir` volumes (ephemeral data is lost). + /// Equivalent to `kubectl drain --delete-emptydir-data`. + pub delete_emptydir_data: bool, + /// Silently skip DaemonSet-managed pods instead of blocking the drain. + /// Equivalent to `kubectl drain --ignore-daemonsets`. + pub ignore_daemonsets: bool, + /// Maximum wall-clock time to wait for all evictions to complete. + pub timeout: Duration, +} + +impl Default for DrainOptions { + fn default() -> Self { + Self { + delete_emptydir_data: false, + ignore_daemonsets: true, + timeout: Duration::from_secs(1), + } + } +} + +impl DrainOptions { + pub fn default_ignore_daemonset_delete_emptydir_data() -> Self { + Self { + delete_emptydir_data: true, + ignore_daemonsets: true, + ..Self::default() + } + } +} + +/// Controls how [`crate::K8sClient::apply_with_strategy`] behaves when the +/// resource already exists (or does not). +pub enum WriteMode { + /// Server-side apply; create if absent, update if present (default). + CreateOrUpdate, + /// POST only; return an error if the resource already exists. + Create, + /// Server-side apply only; return an error if the resource does not exist. + Update, +} + +// ── Scope resolution trait ─────────────────────────────────────────────────── + +/// Resolves the correct [`kube::Api`] for a resource type based on its scope +/// (cluster-wide vs. namespace-scoped). +pub trait ScopeResolver { + fn get_api(client: &Client, ns: Option<&str>) -> Api; +} + +impl ScopeResolver for ClusterResourceScope +where + K: Resource, + ::DynamicType: Default, +{ + fn get_api(client: &Client, _ns: Option<&str>) -> Api { + Api::all(client.clone()) + } +} + +impl ScopeResolver for NamespaceResourceScope +where + K: Resource, + ::DynamicType: Default, +{ + fn get_api(client: &Client, ns: Option<&str>) -> Api { + match ns { + Some(ns) => Api::namespaced(client.clone(), ns), + None => Api::default_namespaced(client.clone()), + } + } +} diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index d154277..baa52be 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -21,6 +21,8 @@ semver = "1.0.23" serde.workspace = true serde_json.workspace = true tokio.workspace = true +tokio-retry.workspace = true +tokio-util.workspace = true derive-new.workspace = true log.workspace = true env_logger.workspace = true @@ -31,6 +33,7 @@ opnsense-config-xml = { path = "../opnsense-config-xml" } harmony_macros = { path = "../harmony_macros" } harmony_types = { path = "../harmony_types" } harmony_execution = { path = "../harmony_execution" } +harmony-k8s = { path = "../harmony-k8s" } uuid.workspace = true url.workspace = true kube = { workspace = true, features = ["derive"] } @@ -60,7 +63,6 @@ temp-dir = "0.1.14" dyn-clone = "1.0.19" similar.workspace = true futures-util = "0.3.31" -tokio-util = "0.7.15" strum = { version = "0.27.1", features = ["derive"] } tempfile.workspace = true serde_with = "3.14.0" @@ -80,7 +82,7 @@ sqlx.workspace = true inquire.workspace = true brocade = { path = "../brocade" } option-ext = "0.2.0" -tokio-retry = "0.3.0" +rand.workspace = true [dev-dependencies] pretty_assertions.workspace = true diff --git a/harmony/src/domain/interpret/mod.rs b/harmony/src/domain/interpret/mod.rs index c5e92ce..4a9855a 100644 --- a/harmony/src/domain/interpret/mod.rs +++ b/harmony/src/domain/interpret/mod.rs @@ -4,8 +4,6 @@ use std::error::Error; use async_trait::async_trait; use derive_new::new; -use crate::inventory::HostRole; - use super::{ data::Version, executors::ExecutorError, inventory::Inventory, topology::PreparationError, }; diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index ea4f2f8..e68e274 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -1,4 +1,5 @@ use async_trait::async_trait; +use harmony_k8s::K8sClient; use harmony_macros::ip; use harmony_types::{ id::Id, @@ -16,7 +17,7 @@ use super::{ DHCPStaticEntry, DhcpServer, DnsRecord, DnsRecordType, DnsServer, Firewall, HostNetworkConfig, HttpServer, IpAddress, K8sclient, LoadBalancer, LoadBalancerService, LogicalHost, NetworkError, NetworkManager, PreparationError, PreparationOutcome, Router, Switch, SwitchClient, - SwitchError, TftpServer, Topology, k8s::K8sClient, + SwitchError, TftpServer, Topology, }; use std::{ process::Command, diff --git a/harmony/src/domain/topology/k8s/mod.rs b/harmony/src/domain/topology/k8s/mod.rs deleted file mode 100644 index f5171ad..0000000 --- a/harmony/src/domain/topology/k8s/mod.rs +++ /dev/null @@ -1,2615 +0,0 @@ -pub mod bundle; -pub mod config; -pub mod helper; - -use std::{ - collections::{BTreeMap, HashMap}, - sync::Arc, - time::{Duration, SystemTime, UNIX_EPOCH}, -}; - -use k8s_openapi::{ - ClusterResourceScope, NamespaceResourceScope, - api::{ - apps::v1::Deployment, - core::v1::{ - ConfigMap, ConfigMapVolumeSource, Node, Pod, ServiceAccount, Volume, VolumeMount, - }, - }, - apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, - apimachinery::pkg::{apis::meta::v1::ObjectMeta, version::Info}, -}; -use kube::{ - Client, Config, Discovery, Error, Resource, - api::{ - Api, AttachParams, DeleteParams, EvictParams, ListParams, ObjectList, Patch, PatchParams, - PostParams, ResourceExt, - }, - config::{KubeConfigOptions, Kubeconfig}, - core::ErrorResponse, - discovery::{ApiCapabilities, Scope}, - error::DiscoveryError, - runtime::reflector::Lookup, -}; -use kube::{api::DynamicObject, runtime::conditions}; -use kube::{ - api::{ApiResource, GroupVersionKind}, - runtime::wait::await_condition, -}; -use log::{debug, error, info, trace, warn}; -use serde::{Serialize, de::DeserializeOwned}; -use serde_json::{Value, json}; -use similar::TextDiff; -use tokio::{ - io::AsyncReadExt, - sync::{Mutex, OnceCell}, - time::sleep, -}; -use tokio_retry::{Retry, strategy::ExponentialBackoff}; -use url::Url; - -use crate::topology::{KubernetesDistribution, k8s::helper::PrivilegedPodConfig}; - -#[derive(Clone)] -pub struct K8sClient { - client: Client, - k8s_distribution: Arc>, - discovery: Arc>, -} - -impl Serialize for K8sClient { - fn serialize(&self, _serializer: S) -> Result - where - S: serde::Serializer, - { - todo!() - } -} - -impl std::fmt::Debug for K8sClient { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // This is a poor man's debug implementation for now as kube::Client does not provide much - // useful information - f.write_fmt(format_args!( - "K8sClient {{ kube client using default namespace {} }}", - self.client.default_namespace() - )) - } -} - -/// A file to be written to a node's filesystem. -#[derive(Debug, Clone)] -pub struct NodeFile { - /// The absolute path on the host where the file should be written. - pub path: String, - /// The content of the file. - pub content: String, - /// The file permissions (e.g. 0o600). - pub mode: u32, -} - -/// Options controlling the behavior of a [`K8sClient::drain_node`] operation. -#[derive(Debug, Clone)] -pub struct DrainOptions { - /// If `true`, pods that use `emptyDir` volumes will be evicted (their - /// ephemeral data is lost). Equivalent to `kubectl drain - /// --delete-emptydir-data`. - pub delete_emptydir_data: bool, - /// If `true`, DaemonSet-managed pods are silently skipped instead of - /// blocking the drain. Equivalent to `kubectl drain --ignore-daemonsets`. - pub ignore_daemonsets: bool, - /// Maximum wall-clock time to wait for all evictions to complete before - /// returning an error. - pub timeout: Duration, -} - -impl Default for DrainOptions { - fn default() -> Self { - Self { - delete_emptydir_data: false, - ignore_daemonsets: true, - // TODO sane timeout - timeout: Duration::from_secs(1), - } - } -} - -impl DrainOptions { - pub fn default_ignore_daemonset_delete_emptydir_data() -> DrainOptions { - let mut drain_opts = DrainOptions::default(); - drain_opts.delete_emptydir_data = true; - drain_opts.ignore_daemonsets = true; - drain_opts - } -} - -impl K8sClient { - pub fn new(client: Client) -> Self { - Self { - client, - k8s_distribution: Arc::new(OnceCell::new()), - discovery: Arc::new(OnceCell::new()), - } - } - - pub async fn try_default() -> Result { - let client = Self { - client: Client::try_default().await?, - k8s_distribution: Arc::new(OnceCell::new()), - discovery: Arc::new(OnceCell::new()), - }; - - Ok(client) - } - - /// Returns true if any deployment in the given namespace matching the label selector - /// has status.availableReplicas > 0 (or condition Available=True). - pub async fn has_healthy_deployment_with_label( - &self, - namespace: &str, - label_selector: &str, - ) -> Result { - let api: Api = Api::namespaced(self.client.clone(), namespace); - let lp = ListParams::default().labels(label_selector); - let list = api.list(&lp).await?; - for d in list.items { - // Check AvailableReplicas > 0 or Available condition - let available = d - .status - .as_ref() - .and_then(|s| s.available_replicas) - .unwrap_or(0); - if available > 0 { - return Ok(true); - } - // Fallback: scan conditions - if let Some(conds) = d.status.as_ref().and_then(|s| s.conditions.as_ref()) { - if conds - .iter() - .any(|c| c.type_ == "Available" && c.status == "True") - { - return Ok(true); - } - } - } - Ok(false) - } - - /// Cluster-wide: returns namespaces that have at least one healthy deployment - /// matching the label selector (equivalent to kubectl -A -l ...). - pub async fn list_namespaces_with_healthy_deployments( - &self, - label_selector: &str, - ) -> Result, Error> { - let api: Api = Api::all(self.client.clone()); - let lp = ListParams::default().labels(label_selector); - let list = api.list(&lp).await?; - - let mut healthy_ns: HashMap = HashMap::new(); - for d in list.items { - let ns = match d.metadata.namespace.clone() { - Some(n) => n, - None => continue, - }; - let available = d - .status - .as_ref() - .and_then(|s| s.available_replicas) - .unwrap_or(0); - let is_healthy = if available > 0 { - true - } else { - d.status - .as_ref() - .and_then(|s| s.conditions.as_ref()) - .map(|conds| { - conds - .iter() - .any(|c| c.type_ == "Available" && c.status == "True") - }) - .unwrap_or(false) - }; - if is_healthy { - healthy_ns.insert(ns, true); - } - } - - Ok(healthy_ns.into_keys().collect()) - } - - /// Get the application-controller ServiceAccount name (fallback to default) - pub async fn get_controller_service_account_name( - &self, - ns: &str, - ) -> Result, Error> { - let api: Api = Api::namespaced(self.client.clone(), ns); - let lp = ListParams::default().labels("app.kubernetes.io/component=controller"); - let list = api.list(&lp).await?; - if let Some(dep) = list.items.get(0) { - if let Some(sa) = dep - .spec - .as_ref() - .and_then(|ds| ds.template.spec.as_ref()) - .and_then(|ps| ps.service_account_name.clone()) - { - return Ok(Some(sa)); - } - } - Ok(None) - } - - // List ClusterRoleBindings dynamically and return as JSON values - pub async fn list_clusterrolebindings_json(&self) -> Result, Error> { - let gvk = kube::api::GroupVersionKind::gvk( - "rbac.authorization.k8s.io", - "v1", - "ClusterRoleBinding", - ); - let ar = kube::api::ApiResource::from_gvk(&gvk); - let api: Api = Api::all_with(self.client.clone(), &ar); - let crbs = api.list(&ListParams::default()).await?; - let mut out = Vec::new(); - for o in crbs { - let v = serde_json::to_value(&o).unwrap_or(Value::Null); - out.push(v); - } - Ok(out) - } - - /// Determine if Argo controller in ns has cluster-wide permissions via CRBs - // TODO This does not belong in the generic k8s client, should be refactored at some point - pub async fn is_service_account_cluster_wide(&self, sa: &str, ns: &str) -> Result { - let crbs = self.list_clusterrolebindings_json().await?; - let sa_user = format!("system:serviceaccount:{}:{}", ns, sa); - for crb in crbs { - if let Some(subjects) = crb.get("subjects").and_then(|s| s.as_array()) { - for subj in subjects { - let kind = subj.get("kind").and_then(|v| v.as_str()).unwrap_or(""); - let name = subj.get("name").and_then(|v| v.as_str()).unwrap_or(""); - let subj_ns = subj.get("namespace").and_then(|v| v.as_str()).unwrap_or(""); - if (kind == "ServiceAccount" && name == sa && subj_ns == ns) - || (kind == "User" && name == sa_user) - { - return Ok(true); - } - } - } - } - Ok(false) - } - - pub async fn has_crd(&self, name: &str) -> Result { - let api: Api = Api::all(self.client.clone()); - let lp = ListParams::default().fields(&format!("metadata.name={}", name)); - let crds = api.list(&lp).await?; - Ok(!crds.items.is_empty()) - } - - pub async fn service_account_api(&self, namespace: &str) -> Api { - let api: Api = Api::namespaced(self.client.clone(), namespace); - api - } - - pub async fn get_apiserver_version(&self) -> Result { - let client: Client = self.client.clone(); - let version_info: Info = client.apiserver_version().await?; - Ok(version_info) - } - - pub async fn discovery(&self) -> Result<&Discovery, Error> { - // Retry with exponential backoff in case of API server load - let retry_strategy = ExponentialBackoff::from_millis(1000) - .max_delay(Duration::from_secs(32)) - .take(6); - - let attempt = Mutex::new(0); - Retry::spawn(retry_strategy, || async { - let mut alock = attempt.lock().await; - *alock += 1; - match self - .discovery - .get_or_try_init(async || { - debug!("Running Kubernetes API discovery (attempt {})", *alock); - let discovery = Discovery::new(self.client.clone()).run().await?; - debug!("Kubernetes API discovery completed"); - Ok(discovery) - }) - .await - { - Ok(discovery) => Ok(discovery), - Err(e) => { - warn!( - "Kubernetes API discovery failed (attempt {}): {}", - *alock, e - ); - Err(e) - } - } - }) - .await - .map_err(|e| { - error!("Kubernetes API discovery failed after all retries: {}", e); - e - }) - } - - pub async fn get_resource_json_value( - &self, - name: &str, - namespace: Option<&str>, - gvk: &GroupVersionKind, - ) -> Result { - let gvk = ApiResource::from_gvk(gvk); - let resource: Api = if let Some(ns) = namespace { - Api::namespaced_with(self.client.clone(), ns, &gvk) - } else { - Api::default_namespaced_with(self.client.clone(), &gvk) - }; - - resource.get(name).await - } - - pub async fn get_secret_json_value( - &self, - name: &str, - namespace: Option<&str>, - ) -> Result { - self.get_resource_json_value( - name, - namespace, - &GroupVersionKind { - group: "".to_string(), - version: "v1".to_string(), - kind: "Secret".to_string(), - }, - ) - .await - } - - pub async fn get_deployment( - &self, - name: &str, - namespace: Option<&str>, - ) -> Result, Error> { - let deps: Api = if let Some(ns) = namespace { - debug!("getting namespaced deployment"); - Api::namespaced(self.client.clone(), ns) - } else { - debug!("getting default namespace deployment"); - Api::default_namespaced(self.client.clone()) - }; - - debug!("getting deployment {} in ns {}", name, namespace.unwrap()); - deps.get_opt(name).await - } - - pub async fn get_pod(&self, name: &str, namespace: Option<&str>) -> Result, Error> { - let pods: Api = if let Some(ns) = namespace { - Api::namespaced(self.client.clone(), ns) - } else { - Api::default_namespaced(self.client.clone()) - }; - - pods.get_opt(name).await - } - - pub async fn scale_deployment( - &self, - name: &str, - namespace: Option<&str>, - replicas: u32, - ) -> Result<(), Error> { - let deployments: Api = if let Some(ns) = namespace { - Api::namespaced(self.client.clone(), ns) - } else { - Api::default_namespaced(self.client.clone()) - }; - - let patch = json!({ - "spec": { - "replicas": replicas - } - }); - let pp = PatchParams::default(); - let scale = Patch::Merge(&patch); - deployments.patch_scale(name, &pp, &scale).await?; - Ok(()) - } - - pub async fn delete_deployment( - &self, - name: &str, - namespace: Option<&str>, - ) -> Result<(), Error> { - let deployments: Api = if let Some(ns) = namespace { - Api::namespaced(self.client.clone(), ns) - } else { - Api::default_namespaced(self.client.clone()) - }; - let delete_params = DeleteParams::default(); - deployments.delete(name, &delete_params).await?; - Ok(()) - } - - pub async fn wait_until_deployment_ready( - &self, - name: &str, - namespace: Option<&str>, - timeout: Option, - ) -> Result<(), String> { - let api: Api; - - if let Some(ns) = namespace { - api = Api::namespaced(self.client.clone(), ns); - } else { - api = Api::default_namespaced(self.client.clone()); - } - - let establish = await_condition(api, name, conditions::is_deployment_completed()); - let timeout = timeout.unwrap_or(Duration::from_secs(120)); - let res = tokio::time::timeout(timeout, establish).await; - - if res.is_ok() { - Ok(()) - } else { - Err("timed out while waiting for deployment".to_string()) - } - } - - pub async fn wait_for_pod_ready( - &self, - pod_name: &str, - namespace: Option<&str>, - ) -> Result<(), Error> { - let mut elapsed = 0; - let interval = 5; // seconds between checks - let timeout_secs = 120; - loop { - let pod = self.get_pod(pod_name, namespace).await?; - - if let Some(p) = pod { - if let Some(status) = p.status { - if let Some(phase) = status.phase { - if phase.to_lowercase() == "running" { - return Ok(()); - } - } - } - } - - if elapsed >= timeout_secs { - return Err(Error::Discovery(DiscoveryError::MissingResource(format!( - "'{}' in ns '{}' did not become ready within {}s", - pod_name, - namespace.unwrap(), - timeout_secs - )))); - } - - sleep(Duration::from_secs(interval)).await; - elapsed += interval; - } - } - - /// Will execute a commond in the first pod found that matches the specified label - /// '{label}={name}' - pub async fn exec_app_capture_output( - &self, - name: String, - label: String, - namespace: Option<&str>, - command: Vec<&str>, - ) -> Result { - let api: Api; - - if let Some(ns) = namespace { - api = Api::namespaced(self.client.clone(), ns); - } else { - api = Api::default_namespaced(self.client.clone()); - } - let pod_list = api - .list(&ListParams::default().labels(format!("{label}={name}").as_str())) - .await - .expect("couldn't get list of pods"); - - let res = api - .exec( - pod_list - .items - .first() - .expect("couldn't get pod") - .name() - .expect("couldn't get pod name") - .into_owned() - .as_str(), - command, - &AttachParams::default().stdout(true).stderr(true), - ) - .await; - match res { - Err(e) => Err(e.to_string()), - Ok(mut process) => { - let status = process - .take_status() - .expect("Couldn't get status") - .await - .expect("Couldn't unwrap status"); - - if let Some(s) = status.status { - let mut stdout_buf = String::new(); - if let Some(mut stdout) = process.stdout() { - stdout - .read_to_string(&mut stdout_buf) - .await - .map_err(|e| format!("Failed to get status stdout {e}"))?; - } - debug!("Status: {} - {:?}", s, status.details); - if s == "Success" { - Ok(stdout_buf) - } else { - Err(s) - } - } else { - Err("Couldn't get inner status of pod exec".to_string()) - } - } - } - } - - /// Will execute a command in the first pod found that matches the label `app.kubernetes.io/name={name}` - pub async fn exec_app( - &self, - name: String, - namespace: Option<&str>, - command: Vec<&str>, - ) -> Result<(), String> { - let api: Api; - - if let Some(ns) = namespace { - api = Api::namespaced(self.client.clone(), ns); - } else { - api = Api::default_namespaced(self.client.clone()); - } - let pod_list = api - .list(&ListParams::default().labels(format!("app.kubernetes.io/name={name}").as_str())) - .await - .expect("couldn't get list of pods"); - - let res = api - .exec( - pod_list - .items - .first() - .expect("couldn't get pod") - .name() - .expect("couldn't get pod name") - .into_owned() - .as_str(), - command, - &AttachParams::default(), - ) - .await; - - match res { - Err(e) => Err(e.to_string()), - Ok(mut process) => { - let status = process - .take_status() - .expect("Couldn't get status") - .await - .expect("Couldn't unwrap status"); - - if let Some(s) = status.status { - debug!("Status: {} - {:?}", s, status.details); - if s == "Success" { Ok(()) } else { Err(s) } - } else { - Err("Couldn't get inner status of pod exec".to_string()) - } - } - } - } - - pub(crate) fn get_api_for_dynamic_object( - &self, - object: &DynamicObject, - ns: Option<&str>, - ) -> Result, Error> { - let api_resource = object - .types - .as_ref() - .and_then(|t| { - let parts: Vec<&str> = t.api_version.split('/').collect(); - match parts.as_slice() { - [version] => Some(ApiResource::from_gvk(&GroupVersionKind::gvk( - "", version, &t.kind, - ))), - [group, version] => Some(ApiResource::from_gvk(&GroupVersionKind::gvk( - group, version, &t.kind, - ))), - _ => None, - } - }) - .ok_or_else(|| { - Error::BuildRequest(kube::core::request::Error::Validation( - "Invalid apiVersion in DynamicObject {object:#?}".to_string(), - )) - })?; - - match ns { - Some(ns) => Ok(Api::namespaced_with(self.client.clone(), ns, &api_resource)), - None => Ok(Api::default_namespaced_with( - self.client.clone(), - &api_resource, - )), - } - } - - pub async fn apply_dynamic_many( - &self, - resource: &[DynamicObject], - namespace: Option<&str>, - force_conflicts: bool, - ) -> Result, Error> { - let mut result = Vec::new(); - for r in resource.iter() { - result.push(self.apply_dynamic(r, namespace, force_conflicts).await?); - } - - Ok(result) - } - - /// Apply DynamicObject resource to the cluster - pub async fn apply_dynamic( - &self, - resource: &DynamicObject, - namespace: Option<&str>, - force_conflicts: bool, - ) -> Result { - // Use discovery to determine the correct API scope - trace!( - "Apply dynamic resource {resource:#?} \n namespace :{namespace:?} force_conflicts {force_conflicts}" - ); - let discovery = self.discovery().await?; - - let type_meta = resource.types.as_ref().ok_or_else(|| { - Error::BuildRequest(kube::core::request::Error::Validation( - "DynamicObject must have types (apiVersion and kind)".to_string(), - )) - })?; - - let gvk = GroupVersionKind::try_from(type_meta).map_err(|_| { - Error::BuildRequest(kube::core::request::Error::Validation(format!( - "Invalid GroupVersionKind in DynamicObject: {:?}", - type_meta - ))) - })?; - - let (ar, caps) = discovery.resolve_gvk(&gvk).ok_or_else(|| { - Error::Discovery(DiscoveryError::MissingResource(format!( - "Cannot resolve GVK: {:?}", - gvk - ))) - })?; - - // Determine namespace based on resource scope - let effective_namespace = if caps.scope == Scope::Cluster { - None - } else { - namespace.or_else(|| resource.metadata.namespace.as_deref()) - }; - - trace!( - "Discovered information ar {ar:?}, caps {caps:?}, effective_namespace {effective_namespace:?}" - ); - - // Build API using discovered resource and capabilities - let api = get_dynamic_api(ar, caps, self.client.clone(), effective_namespace, false); - let name = resource - .metadata - .name - .as_ref() - .ok_or_else(|| { - Error::BuildRequest(kube::core::request::Error::Validation( - "DynamicObject must have metadata.name".to_string(), - )) - })? - .as_str(); - - debug!( - "Applying dynamic resource kind={:?} apiVersion={:?} name='{}' ns={:?}", - resource.types.as_ref().map(|t| &t.kind), - resource.types.as_ref().map(|t| &t.api_version), - name, - namespace - ); - trace!( - "Dynamic resource payload:\n{:#}", - serde_json::to_value(resource).unwrap_or(serde_json::Value::Null) - ); - - // Using same field manager as in apply() - let mut patch_params = PatchParams::apply("harmony"); - patch_params.force = force_conflicts; - - if *crate::config::DRY_RUN { - // Dry-run path: fetch current, show diff, and return appropriate object - match api.get(name).await { - Ok(current) => { - trace!("Received current dynamic value {current:#?}"); - - println!("\nPerforming dry-run for resource: '{}'", name); - - // Serialize current and new, and strip status from current if present - let mut current_yaml = - serde_yaml::to_value(¤t).unwrap_or_else(|_| serde_yaml::Value::Null); - if let Some(map) = current_yaml.as_mapping_mut() { - if map.contains_key(&serde_yaml::Value::String("status".to_string())) { - let removed = - map.remove(&serde_yaml::Value::String("status".to_string())); - trace!("Removed status from current dynamic object: {:?}", removed); - } else { - trace!( - "Did not find status entry for current dynamic object {}/{}", - current.metadata.namespace.as_deref().unwrap_or(""), - current.metadata.name.as_deref().unwrap_or("") - ); - } - } - - let current_yaml = serde_yaml::to_string(¤t_yaml) - .unwrap_or_else(|_| "Failed to serialize current resource".to_string()); - let new_yaml = serde_yaml::to_string(resource) - .unwrap_or_else(|_| "Failed to serialize new resource".to_string()); - - if current_yaml == new_yaml { - println!("No changes detected."); - return Ok(current); - } - - println!("Changes detected:"); - let diff = TextDiff::from_lines(¤t_yaml, &new_yaml); - for change in diff.iter_all_changes() { - let sign = match change.tag() { - similar::ChangeTag::Delete => "-", - similar::ChangeTag::Insert => "+", - similar::ChangeTag::Equal => " ", - }; - print!("{}{}", sign, change); - } - - // Return the incoming resource as the would-be applied state - Ok(resource.clone()) - } - Err(Error::Api(ErrorResponse { code: 404, .. })) => { - println!("\nPerforming dry-run for new resource: '{}'", name); - println!( - "Resource does not exist. It would be created with the following content:" - ); - let new_yaml = serde_yaml::to_string(resource) - .unwrap_or_else(|_| "Failed to serialize new resource".to_string()); - for line in new_yaml.lines() { - println!("+{}", line); - } - Ok(resource.clone()) - } - Err(e) => { - error!("Failed to get dynamic resource '{}': {}", name, e); - Err(e) - } - } - } else { - // Real apply via server-side apply - // Server-side apply works for both create and update operations - debug!("Applying (server-side apply) dynamic resource '{}'", name); - match api - .patch(name, &patch_params, &Patch::Apply(resource)) - .await - { - Ok(obj) => Ok(obj), - Err(Error::Api(ErrorResponse { code: 404, .. })) => { - // Resource doesn't exist, server-side apply should create it - // This can happen with some API servers, so we explicitly create - debug!("Resource '{}' not found, creating via POST", name); - trace!("{resource:#?}"); - api.create(&PostParams::default(), resource) - .await - .map_err(|e| { - error!("Failed to create dynamic resource '{}': {}", name, e); - e - }) - } - Err(e) => { - error!("Failed to apply dynamic resource '{}': {}", name, e); - Err(e) - } - } - } - } - - /// Apply a resource in namespace - /// - /// See `kubectl apply` for more information on the expected behavior of this function - pub async fn apply(&self, resource: &K, namespace: Option<&str>) -> Result - where - K: Resource + Clone + std::fmt::Debug + DeserializeOwned + serde::Serialize, - ::DynamicType: Default, - { - debug!( - "Applying resource {:?} with ns {:?}", - resource.meta().name, - namespace - ); - trace!( - "{:#}", - serde_json::to_value(resource).unwrap_or(serde_json::Value::Null) - ); - - // ── 1. Extract GVK from compile-time type info ────────────────────────── - let dyntype = K::DynamicType::default(); - let gvk = GroupVersionKind { - group: K::group(&dyntype).to_string(), - version: K::version(&dyntype).to_string(), - kind: K::kind(&dyntype).to_string(), - }; - - // ── 2. Resolve scope at runtime via discovery ──────────────────────────── - let discovery = self.discovery().await?; - let (ar, caps) = discovery.resolve_gvk(&gvk).ok_or_else(|| { - Error::Discovery(DiscoveryError::MissingResource(format!( - "Cannot resolve GVK: {:?}", - gvk - ))) - })?; - - let effective_namespace = if caps.scope == Scope::Cluster { - None - } else { - // Prefer the caller-supplied namespace, fall back to the resource's own - namespace.or_else(|| resource.meta().namespace.as_deref()) - }; - - // ── 3. Determine the effective namespace based on the discovered scope ─── - let api: Api = - get_dynamic_api(ar, caps, self.client.clone(), effective_namespace, false); - - let patch_params = PatchParams::apply("harmony"); - let name = resource - .meta() - .name - .as_ref() - .expect("K8s Resource should have a name"); - - if *crate::config::DRY_RUN { - match api.get(name).await { - Ok(current) => { - trace!("Received current value {current:#?}"); - // The resource exists, so we calculate and display a diff. - println!("\nPerforming dry-run for resource: '{name}'"); - let mut current_yaml = serde_yaml::to_value(¤t).unwrap_or_else(|_| { - panic!("Could not serialize current value : {current:#?}") - }); - if current_yaml.is_mapping() && current_yaml.get("status").is_some() { - let map = current_yaml.as_mapping_mut().unwrap(); - let removed = map.remove_entry("status"); - trace!("Removed status {removed:?}"); - } else { - trace!( - "Did not find status entry for current object {}/{}", - current.meta().namespace.as_ref().unwrap_or(&"".to_string()), - current.meta().name.as_ref().unwrap_or(&"".to_string()) - ); - } - let current_yaml = serde_yaml::to_string(¤t_yaml) - .unwrap_or_else(|_| "Failed to serialize current resource".to_string()); - let new_yaml = serde_yaml::to_string(resource) - .unwrap_or_else(|_| "Failed to serialize new resource".to_string()); - - if current_yaml == new_yaml { - println!("No changes detected."); - // Return the current resource state as there are no changes. - return helper::dyn_to_typed(current); - } - - println!("Changes detected:"); - let diff = TextDiff::from_lines(¤t_yaml, &new_yaml); - - // Iterate over the changes and print them in a git-like diff format. - for change in diff.iter_all_changes() { - let sign = match change.tag() { - similar::ChangeTag::Delete => "-", - similar::ChangeTag::Insert => "+", - similar::ChangeTag::Equal => " ", - }; - print!("{sign}{change}"); - } - // In a dry run, we return the new resource state that would have been applied. - Ok(resource.clone()) - } - Err(Error::Api(ErrorResponse { code: 404, .. })) => { - // The resource does not exist, so the "diff" is the entire new resource. - println!("\nPerforming dry-run for new resource: '{name}'"); - println!( - "Resource does not exist. It would be created with the following content:" - ); - let new_yaml = serde_yaml::to_string(resource) - .unwrap_or_else(|_| "Failed to serialize new resource".to_string()); - - // Print each line of the new resource with a '+' prefix. - for line in new_yaml.lines() { - println!("+{line}"); - } - // In a dry run, we return the new resource state that would have been created. - Ok(resource.clone()) - } - Err(e) => { - // Another API error occurred. - error!("Failed to get resource '{name}': {e}"); - Err(e) - } - } - } else { - // Real apply via server-side apply - // Server-side apply works for both create and update operations - match api - .patch(name, &patch_params, &Patch::Apply(resource)) - .await - { - Ok(obj) => helper::dyn_to_typed(obj), - Err(Error::Api(ErrorResponse { code: 404, .. })) => { - // Resource doesn't exist, server-side apply should create it - // This can happen with some API servers, so we explicitly create - debug!("Resource '{}' not found, creating via POST", name); - let dyn_resource: DynamicObject = serde_json::from_value( - serde_json::to_value(resource).map_err(Error::SerdeError)?, - ) - .map_err(Error::SerdeError)?; - - api.create(&PostParams::default(), &dyn_resource) - .await - .and_then(helper::dyn_to_typed) - .map_err(|e| { - error!("Failed to create resource '{}': {}", name, e); - e - }) - } - Err(e) => { - error!("Failed to apply resource '{}': {}", name, e); - Err(e) - } - } - } - } - - pub async fn apply_many(&self, resource: &[K], ns: Option<&str>) -> Result, Error> - where - K: Resource + Clone + std::fmt::Debug + DeserializeOwned + serde::Serialize, - ::DynamicType: Default, - { - let mut result = Vec::new(); - for r in resource.iter() { - let apply_result = self.apply(r, ns).await; - if apply_result.is_err() { - // NOTE : We should be careful about this one, it may leak sensitive information in - // logs - // Maybe just reducing it to debug would be enough as we already know debug logs - // are unsafe. - // But keeping it at warn makes it much easier to understand what is going on. So be it for now. - warn!( - "Failed to apply k8s resource : {}", - serde_json::to_string_pretty(r).map_err(|e| Error::SerdeError(e))? - ); - } - - result.push(apply_result?); - } - - Ok(result) - } - - pub async fn apply_yaml_many( - &self, - #[allow(clippy::ptr_arg)] yaml: &Vec, - ns: Option<&str>, - ) -> Result<(), Error> { - for y in yaml.iter() { - self.apply_yaml(y, ns).await?; - } - Ok(()) - } - - pub async fn apply_yaml( - &self, - yaml: &serde_yaml::Value, - ns: Option<&str>, - ) -> Result<(), Error> { - let obj: DynamicObject = serde_yaml::from_value(yaml.clone()).expect("TODO do not unwrap"); - let name = obj.metadata.name.as_ref().expect("YAML must have a name"); - - let api_version = yaml - .get("apiVersion") - .expect("couldn't get apiVersion from YAML") - .as_str() - .expect("couldn't get apiVersion as str"); - let kind = yaml - .get("kind") - .expect("couldn't get kind from YAML") - .as_str() - .expect("couldn't get kind as str"); - - let mut it = api_version.splitn(2, '/'); - let first = it.next().unwrap(); - let (g, v) = match it.next() { - Some(second) => (first, second), - None => ("", first), - }; - - let gvk = GroupVersionKind::gvk(g, v, kind); - let api_resource = ApiResource::from_gvk(&gvk); - - let namespace = match ns { - Some(n) => n, - None => obj - .metadata - .namespace - .as_ref() - .expect("YAML must have a namespace"), - }; - - // 5. Create a dynamic API client for this resource type. - let api: Api = - Api::namespaced_with(self.client.clone(), namespace, &api_resource); - - // 6. Apply the object to the cluster using Server-Side Apply. - // This will create the resource if it doesn't exist, or update it if it does. - println!("Applying '{name}' in namespace '{namespace}'...",); - let patch_params = PatchParams::apply("harmony"); // Use a unique field manager name - let result = api.patch(name, &patch_params, &Patch::Apply(&obj)).await?; - - println!("Successfully applied '{}'.", result.name_any()); - - Ok(()) - } - - /// Apply a resource from a URL - /// - /// It is the equivalent of `kubectl apply -f ` - pub async fn apply_url(&self, url: Url, ns: Option<&str>) -> Result<(), Error> { - let patch_params = PatchParams::apply("harmony"); - let discovery = self.discovery().await?; - - let yaml = reqwest::get(url) - .await - .expect("Could not get URL") - .text() - .await - .expect("Could not get content from URL"); - - for doc in multidoc_deserialize(&yaml).expect("failed to parse YAML from file") { - let obj: DynamicObject = - serde_yaml::from_value(doc).expect("cannot apply without valid YAML"); - let namespace = obj.metadata.namespace.as_deref().or(ns); - let type_meta = obj - .types - .as_ref() - .expect("cannot apply object without valid TypeMeta"); - let gvk = GroupVersionKind::try_from(type_meta) - .expect("cannot apply object without valid GroupVersionKind"); - let name = obj.name_any(); - - if let Some((ar, caps)) = discovery.resolve_gvk(&gvk) { - let api = get_dynamic_api(ar, caps, self.client.clone(), namespace, false); - trace!( - "Applying {}: \n{}", - gvk.kind, - serde_yaml::to_string(&obj).expect("Failed to serialize YAML") - ); - let data: serde_json::Value = - serde_json::to_value(&obj).expect("Failed to serialize JSON"); - let _r = api.patch(&name, &patch_params, &Patch::Apply(data)).await?; - debug!("applied {} {}", gvk.kind, name); - } else { - warn!("Cannot apply document for unknown {gvk:?}"); - } - } - - Ok(()) - } - - /// Gets a single named resource of a specific type `K`. - /// - /// This function uses the `ApplyStrategy` trait to correctly determine - /// whether to look in a specific namespace or in the entire cluster. - /// - /// Returns `Ok(None)` if the resource is not found (404). - pub async fn get_resource( - &self, - name: &str, - namespace: Option<&str>, - ) -> Result, Error> - where - K: Resource + Clone + std::fmt::Debug + DeserializeOwned, - ::Scope: ApplyStrategy, - ::DynamicType: Default, - { - let api: Api = - <::Scope as ApplyStrategy>::get_api(&self.client, namespace); - - api.get_opt(name).await - } - - pub async fn list_all_resources_with_labels(&self, labels: &str) -> Result, Error> - where - K: Resource + Clone + std::fmt::Debug + DeserializeOwned, - ::DynamicType: Default, - { - let api: Api = Api::all(self.client.clone()); - - let lp = ListParams::default().labels(labels); - Ok(api.list(&lp).await?.items) - } - - pub async fn get_all_resource_in_all_namespace(&self) -> Result, Error> - where - K: Resource + Clone + std::fmt::Debug + DeserializeOwned, - ::Scope: ApplyStrategy, - ::DynamicType: Default, - { - let api: Api = Api::all(self.client.clone()); - Ok(api.list(&Default::default()).await?.items) - } - - /// Lists all resources of a specific type `K`. - /// - /// This function uses the `ApplyStrategy` trait to correctly determine - /// whether to list from a specific namespace or from the entire cluster. - pub async fn list_resources( - &self, - namespace: Option<&str>, - list_params: Option, - ) -> Result, Error> - where - K: Resource + Clone + std::fmt::Debug + DeserializeOwned, - ::Scope: ApplyStrategy, - ::DynamicType: Default, - { - let api: Api = - <::Scope as ApplyStrategy>::get_api(&self.client, namespace); - - let list_params = list_params.unwrap_or_default(); - api.list(&list_params).await - } - - /// Fetches a list of all Nodes in the cluster. - pub async fn get_nodes( - &self, - list_params: Option, - ) -> Result, Error> { - self.list_resources(None, list_params).await - } - - pub async fn from_kubeconfig(path: &str) -> Option { - Self::from_kubeconfig_with_opts(path, &KubeConfigOptions::default()).await - } - - pub async fn from_kubeconfig_with_context( - path: &str, - context: Option, - ) -> Option { - let mut opts = KubeConfigOptions::default(); - opts.context = context; - - Self::from_kubeconfig_with_opts(path, &opts).await - } - - pub async fn from_kubeconfig_with_opts( - path: &str, - opts: &KubeConfigOptions, - ) -> Option { - let k = match Kubeconfig::read_from(path) { - Ok(k) => k, - Err(e) => { - error!("Failed to load kubeconfig from {path} : {e}"); - return None; - } - }; - - Some(K8sClient::new( - Client::try_from(Config::from_custom_kubeconfig(k, &opts).await.unwrap()).unwrap(), - )) - } - - pub async fn cordon_node(&self, node_name: &str) -> Result<(), Error> { - let api: Api = Api::all(self.client.clone()); - - api.cordon(node_name).await?; - Ok(()) - } - - pub async fn uncordon_node(&self, node_name: &str) -> Result<(), Error> { - let api: Api = Api::all(self.client.clone()); - - api.uncordon(node_name).await?; - - Ok(()) - } - - /// Lists every pod currently scheduled on `node_name`. - async fn list_pods_on_node(&self, node_name: &str) -> Result, Error> { - let api: Api = Api::all(self.client.clone()); - let lp = ListParams::default().fields(&format!("spec.nodeName={}", node_name)); - Ok(api.list(&lp).await?.items) - } - - /// Returns `true` when the pod is a *mirror pod* (a static manifest - /// managed directly by the kubelet). - fn is_mirror_pod(pod: &Pod) -> bool { - pod.metadata - .annotations - .as_ref() - .map(|a| a.contains_key("kubernetes.io/config.mirror")) - .unwrap_or(false) - } - - /// Returns `true` when the pod is owned by a `DaemonSet`. - fn is_daemonset_pod(pod: &Pod) -> bool { - pod.metadata - .owner_references - .as_ref() - .map(|refs| refs.iter().any(|r| r.kind == "DaemonSet")) - .unwrap_or(false) - } - - /// Returns `true` when the pod spec contains at least one `emptyDir` - /// volume. - fn has_emptydir_volume(pod: &Pod) -> bool { - pod.spec - .as_ref() - .and_then(|s| s.volumes.as_ref()) - .map(|vols| vols.iter().any(|v| v.empty_dir.is_some())) - .unwrap_or(false) - } - - /// Returns `true` when the pod has already terminated (`Succeeded` or - /// `Failed`). - fn is_completed_pod(pod: &Pod) -> bool { - pod.status - .as_ref() - .and_then(|s| s.phase.as_deref()) - .map(|phase| phase == "Succeeded" || phase == "Failed") - .unwrap_or(false) - } - - /// Partitions `pods` into *(evictable, skipped_descriptions)*. - /// - /// Returns `Err` with a human-readable message when one or more pods would - /// block the drain (e.g. a `DaemonSet` pod with `ignore_daemonsets = - /// false`). - fn classify_pods_for_drain( - pods: &[Pod], - options: &DrainOptions, - ) -> Result<(Vec, Vec), String> { - let mut evictable: Vec = Vec::new(); - let mut skipped: Vec = Vec::new(); - let mut blocking: Vec = Vec::new(); - - for pod in pods { - let name = pod.metadata.name.as_deref().unwrap_or(""); - let ns = pod.metadata.namespace.as_deref().unwrap_or(""); - let qualified = format!("{}/{}", ns, name); - - // Mirror pods are managed by the kubelet — never evict. - if Self::is_mirror_pod(pod) { - skipped.push(format!("{} (mirror pod)", qualified)); - continue; - } - - // Already-terminated pods do not need eviction. - if Self::is_completed_pod(pod) { - skipped.push(format!("{} (completed)", qualified)); - continue; - } - - // DaemonSet pods: skip or block depending on options. - if Self::is_daemonset_pod(pod) { - if options.ignore_daemonsets { - skipped.push(format!("{} (DaemonSet-managed)", qualified)); - } else { - blocking.push(format!( - "{} is managed by a DaemonSet (set ignore_daemonsets to skip)", - qualified - )); - } - continue; - } - - // Pods with emptyDir data: block unless explicitly allowed. - if Self::has_emptydir_volume(pod) && !options.delete_emptydir_data { - blocking.push(format!( - "{} uses emptyDir volumes (set delete_emptydir_data to allow eviction)", - qualified - )); - continue; - } - - evictable.push(pod.clone()); - } - - if !blocking.is_empty() { - return Err(format!( - "Cannot drain node — the following pods block eviction:\n - {}", - blocking.join("\n - ") - )); - } - - Ok((evictable, skipped)) - } - - async fn wait_for_pod_completion(&self, name: &str, namespace: &str) -> Result { - let pod_api: Api = Api::namespaced(self.client.clone(), namespace); - let poll_interval = Duration::from_secs(2); - for _ in 0..60 { - // 2 minutes timeout - sleep(poll_interval).await; - let p = pod_api.get(name).await?; - if let Some(status) = p.status { - match status.phase.as_deref() { - Some("Succeeded") => { - // Capture pod logs as output - let logs = pod_api - .logs(name, &Default::default()) - .await - .unwrap_or_else(|_| String::new()); - - debug!("Retrieved pod {namespace}/{name} logs {logs}"); - - return Ok(logs); - } - Some("Failed") => { - let logs = pod_api - .logs(name, &Default::default()) - .await - .unwrap_or_else(|_| String::new()); - - debug!("Retrieved failed pod {namespace}/{name} logs {logs}"); - - return Err(Error::Discovery(DiscoveryError::MissingResource(format!( - "Pod {} failed. Logs:\n{}", - name, logs - )))); - } - _ => {} - } - } - } - Err(Error::Discovery(DiscoveryError::MissingResource(format!( - "Timed out waiting for pod {}", - name - )))) - } - - pub async fn get_k8s_distribution(&self) -> Result { - self.k8s_distribution - .get_or_try_init(async || { - debug!("Trying to detect k8s distribution"); - let api_groups = self.client.list_api_groups().await?; - trace!("list_api_groups {:?}", api_groups); - debug!("K8s discovery completed"); - - let version = self.get_apiserver_version().await?; - - // OpenShift / OKD - if api_groups - .groups - .iter() - .any(|g| g.name == "project.openshift.io") - { - info!("Found KubernetesDistribution OpenshiftFamily"); - return Ok(KubernetesDistribution::OpenshiftFamily); - } - - // K3d / K3s - if version.git_version.contains("k3s") { - info!("Found KubernetesDistribution K3sFamily"); - return Ok(KubernetesDistribution::K3sFamily); - } - - info!("Could not identify KubernetesDistribution, using Default"); - return Ok(KubernetesDistribution::Default); - }) - .await - .map(|k| k.clone()) - } - - /// Writes a set of files to a node's filesystem using a privileged ephemeral pod. - /// - /// This method creates a ConfigMap containing the file contents and a privileged Pod - /// that mounts the host filesystem. It then copies the files from the ConfigMap - /// to the specified paths on the host and sets the requested permissions. - /// - /// On OpenShift clusters, the required SCC binding is automatically created via - /// the ResourceBundle pattern. - /// - /// ## Use Case: Network Bonding Configuration (ADR-019) - /// - /// This method is designed to support operations like writing NetworkManager - /// configuration files to `/etc/NetworkManager/system-connections/` for - /// setting up LACP bonds on worker nodes, where interface names vary across - /// hardware. - /// - /// Files written via this method persist across reboots on Fedora CoreOS/SCOS. - /// - /// # Arguments - /// - /// * `node_name` - The name of the node to write files to - /// * `files` - A slice of [`NodeFile`] structs containing path, content, and permissions - /// - /// # Example - /// - /// ```rust,no_run - /// # use harmony::topology::k8s::{K8sClient, NodeFile}; - /// # async fn example(client: K8sClient) { - /// let bond_config = NodeFile { - /// path: "/etc/NetworkManager/system-connections/bond0.nmconnection".to_string(), - /// content: "[connection]\nid=bond0\n...".to_string(), - /// mode: 0o600, - /// }; - /// - /// client.write_files_to_node("worker-01", &[bond_config]).await.unwrap(); - /// # } - /// ``` - pub async fn write_files_to_node( - &self, - node_name: &str, - files: &[NodeFile], - ) -> Result { - let ns = self.client.default_namespace(); - let suffix = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis(); - let name = format!("harmony-writer-{}", suffix); - - debug!( - "Preparing to write {} files to node '{}'", - files.len(), - node_name - ); - - // 1. Prepare ConfigMap data & Script - let mut data = BTreeMap::new(); - let mut script = String::from("set -e\n"); - - for (i, file) in files.iter().enumerate() { - let key = format!("f{}", i); - data.insert(key.clone(), file.content.clone()); - - // Ensure parent dir exists - script.push_str(&format!("mkdir -p \"$(dirname \"/host{}\")\"\n", file.path)); - // Copy file - script.push_str(&format!("cp \"/payload/{}\" \"/host{}\"\n", key, file.path)); - // Chmod (format as octal) - script.push_str(&format!("chmod {:o} \"/host{}\"\n", file.mode, file.path)); - } - - let cm = ConfigMap { - metadata: ObjectMeta { - name: Some(name.clone()), - namespace: Some(ns.to_string()), - ..Default::default() - }, - data: Some(data), - ..Default::default() - }; - - let cm_api: Api = Api::namespaced(self.client.clone(), ns); - cm_api.create(&PostParams::default(), &cm).await?; - debug!("Created ConfigMap {}", name); - - // 2. Build resource bundle with Pod and RBAC - let (host_vol, host_mount) = helper::host_root_volume(); - - let payload_vol = Volume { - name: "payload".to_string(), - config_map: Some(ConfigMapVolumeSource { - name: name.clone(), - ..Default::default() - }), - ..Default::default() - }; - - let payload_mount = VolumeMount { - name: "payload".to_string(), - mount_path: "/payload".to_string(), - ..Default::default() - }; - - let bundle = helper::build_privileged_bundle( - PrivilegedPodConfig { - name: name.clone(), - namespace: ns.to_string(), - node_name: node_name.to_string(), - container_name: "writer".to_string(), - command: vec!["/bin/bash".to_string(), "-c".to_string(), script], - volumes: vec![payload_vol, host_vol], - volume_mounts: vec![payload_mount, host_mount], - host_pid: false, - host_network: false, - }, - &self.get_k8s_distribution().await?, - ); - - // 3. Apply bundle (RBAC + Pod) - bundle.apply(self).await?; - debug!("Created privileged pod bundle {}", name); - - // 4. Wait for completion - let result = self.wait_for_pod_completion(&name, ns).await; - - // 5. Cleanup - debug!("Cleaning up resources for {}", name); - let _ = bundle.delete(self).await; - let _ = cm_api.delete(&name, &DeleteParams::default()).await; - - result - } - - /// Runs a privileged command on a specific node using an ephemeral pod. - /// - /// This method creates a privileged pod with host PID and network namespaces - /// enabled, along with the host filesystem mounted at `/host`. The pod runs - /// the specified command and waits for completion. - /// - /// On OpenShift clusters, the required SCC binding is automatically created via - /// the ResourceBundle pattern. - /// - /// # Arguments - /// - /// * `node_name` - The name of the node to run the command on - /// * `command` - The shell command to execute (runs in `/bin/bash -c`) - /// - /// # Returns - /// - /// The stdout output from the command execution. - /// - /// # Example - /// - /// ```rust,no_run - /// # use harmony::topology::k8s::K8sClient; - /// # async fn example(client: K8sClient) { - /// // Reload NetworkManager configuration after writing .nmconnection files - /// let output = client.run_privileged_command_on_node( - /// "worker-01", - /// "nmcli connection reload" - /// ).await.unwrap(); - /// # } - /// ``` - pub async fn run_privileged_command_on_node( - &self, - node_name: &str, - command: &str, - ) -> Result { - let namespace = self.client.default_namespace(); - let suffix = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis(); - let name = format!("harmony-cmd-{}", suffix); - - debug!( - "Running privileged command on node '{}': {}", - node_name, command - ); - - // Build resource bundle with Pod and RBAC - let (host_vol, host_mount) = helper::host_root_volume(); - trace!("Got host volume {host_vol:#?}"); - trace!("Got host volume mount {host_mount:#?}"); - let bundle = helper::build_privileged_bundle( - PrivilegedPodConfig { - name: name.clone(), - namespace: namespace.to_string(), - node_name: node_name.to_string(), - container_name: "runner".to_string(), - command: vec![ - "/bin/bash".to_string(), - "-c".to_string(), - command.to_string(), - ], - volumes: vec![host_vol], - volume_mounts: vec![host_mount], - host_pid: true, - host_network: true, - }, - &self.get_k8s_distribution().await?, - ); - - debug!("Built privileged bundle {bundle:#?}"); - debug!("Built privileged bundle for command : {command}"); - - // Apply bundle (RBAC + Pod) - bundle.apply(self).await?; - debug!("Created privileged pod bundle {}", name); - - // Wait for completion - let result = self.wait_for_pod_completion(&name, namespace).await; - - // Cleanup - debug!("Cleaning up resources for {}", name); - let _ = bundle.delete(self).await; - - result - } - - /// Reboots a Kubernetes node safely with proper drain/uncordon cycle. - /// - /// This method implements a robust node reboot procedure: - /// 1. Records the current boot ID from node status - /// 2. Drains the node (cordons + evicts all pods) - /// 3. Issues a delayed reboot command (fire-and-forget) - /// 4. Waits for the node to go NotReady (confirms shutdown started) - /// 5. Waits for the node to become Ready again - /// 6. Verifies the boot ID changed (confirms actual reboot occurred) - /// 7. Uncordons the node - /// - /// # Arguments - /// - /// * `node_name` - The name of the node to reboot - /// * `drain_options` - Options controlling pod eviction behavior - /// * `timeout` - Maximum time to wait for the entire reboot cycle - /// - /// # Example - /// - /// ```rust,no_run - /// # use harmony::topology::k8s::{K8sClient, DrainOptions}; - /// # use std::time::Duration; - /// # async fn example(client: K8sClient) { - /// client.reboot_node( - /// "worker-01", - /// &DrainOptions::default_ignore_daemonset_delete_emptydir_data(), - /// Duration::from_secs(3600) // 1 hour timeout - /// ).await.unwrap(); - /// # } - /// ``` - pub async fn reboot_node( - &self, - node_name: &str, - drain_options: &DrainOptions, - timeout: Duration, - ) -> Result<(), Error> { - info!("Starting reboot procedure for node '{}'", node_name); - - // 1. Get current boot ID from node status - let node_api: Api = Api::all(self.client.clone()); - let node = node_api.get(node_name).await?; - let boot_id_before = node - .status - .as_ref() - .and_then(|s| s.node_info.as_ref()) - .and_then(|ni| Some(ni.boot_id.clone())) - .ok_or_else(|| { - Error::Discovery(DiscoveryError::MissingResource(format!( - "Node '{}' does not have boot_id in status", - node_name - ))) - })?; - debug!( - "Current boot_id for node '{}': {}", - node_name, boot_id_before - ); - - // 2. Drain the node - info!("Draining node '{}'...", node_name); - self.drain_node(node_name, drain_options).await?; - - let start_time = tokio::time::Instant::now(); - - // 3. Issue delayed reboot command (fire-and-forget) - info!("Scheduling reboot for node '{}'...", node_name); - let reboot_cmd = - "echo rebooting ; nohup bash -c 'sleep 5 && nsenter -t 1 -m -- systemctl reboot'"; - - // Ignore errors - the pod will die during shutdown and we can't wait for completion - match self - .run_privileged_command_on_node(node_name, reboot_cmd) - .await - { - Ok(_) => debug!("Reboot command scheduled successfully"), - Err(e) => { - // This is expected - the node may start shutting down before we can read the pod status - debug!( - "Reboot command scheduling completed with error (expected): {}", - e - ); - } - } - - // 4. Wait for node to go NotReady (proves shutdown started) - info!("Waiting for node '{}' to begin shutdown...", node_name); - let remaining_timeout = timeout.saturating_sub(start_time.elapsed()); - self.wait_for_node_not_ready(node_name, remaining_timeout) - .await?; - - if start_time.elapsed() > timeout { - return Err(Error::Discovery(DiscoveryError::MissingResource(format!( - "Timeout during node '{}' reboot (shutdown detection phase)", - node_name - )))); - } - - // 5. Wait for node to become Ready again - info!("Waiting for node '{}' to come back online...", node_name); - let remaining_timeout = timeout.saturating_sub(start_time.elapsed()); - self.wait_for_node_ready_with_timeout(node_name, remaining_timeout) - .await?; - - if start_time.elapsed() > timeout { - return Err(Error::Discovery(DiscoveryError::MissingResource(format!( - "Timeout during node '{}' reboot (ready phase)", - node_name - )))); - } - - // 6. Verify boot ID changed (confirms actual reboot) - info!("Verifying node '{}' actually rebooted...", node_name); - let node = node_api.get(node_name).await?; - let boot_id_after = node - .status - .as_ref() - .and_then(|s| s.node_info.as_ref()) - .and_then(|ni| Some(ni.boot_id.clone())) - .ok_or_else(|| { - Error::Discovery(DiscoveryError::MissingResource(format!( - "Node '{}' does not have boot_id in status after reboot", - node_name - ))) - })?; - - if boot_id_before == boot_id_after { - return Err(Error::Discovery(DiscoveryError::MissingResource(format!( - "Node '{}' did not actually reboot (boot_id unchanged: {})", - node_name, boot_id_before - )))); - } - - debug!( - "Node '{}' boot_id changed: {} -> {}", - node_name, boot_id_before, boot_id_after - ); - - // 7. Uncordon the node - info!("Uncordoning node '{}'...", node_name); - self.uncordon_node(node_name).await?; - - info!( - "Successfully rebooted node '{}' (took {:?})", - node_name, - start_time.elapsed() - ); - - Ok(()) - } - - /// Waits for a node to transition to NotReady status. - /// - /// This is useful for detecting when a node shutdown has begun. - async fn wait_for_node_not_ready( - &self, - node_name: &str, - timeout: Duration, - ) -> Result<(), Error> { - let api: Api = Api::all(self.client.clone()); - let poll_interval = Duration::from_secs(5); - let start = tokio::time::Instant::now(); - - loop { - if start.elapsed() > timeout { - return Err(Error::Discovery(DiscoveryError::MissingResource(format!( - "Node '{}' did not become NotReady within {:?}", - node_name, timeout - )))); - } - - match api.get(node_name).await { - Ok(node) => { - if let Some(status) = node.status { - if let Some(conditions) = status.conditions { - let is_ready = conditions - .iter() - .any(|cond| cond.type_ == "Ready" && cond.status == "True"); - - if !is_ready { - debug!("Node '{}' is now NotReady", node_name); - return Ok(()); - } - } - } - } - Err(e) => { - debug!("Error checking node '{}' status: {}", node_name, e); - } - } - - sleep(poll_interval).await; - } - } - - pub async fn wait_for_node_ready(&self, node_name: &str) -> Result<(), Error> { - // Default 10 minute timeout for backwards compatibility - self.wait_for_node_ready_with_timeout(node_name, Duration::from_secs(600)) - .await - } - - /// Waits for a node to become Ready with a custom timeout. - async fn wait_for_node_ready_with_timeout( - &self, - node_name: &str, - timeout: Duration, - ) -> Result<(), Error> { - let api: Api = Api::all(self.client.clone()); - let poll_interval = Duration::from_secs(5); - let start = tokio::time::Instant::now(); - - loop { - if start.elapsed() > timeout { - return Err(Error::Discovery(DiscoveryError::MissingResource(format!( - "Node '{}' did not become ready within {:?}", - node_name, timeout - )))); - } - - match api.get(node_name).await { - Ok(node) => { - if let Some(status) = node.status { - if let Some(conditions) = status.conditions { - for cond in conditions { - if cond.type_ == "Ready" && cond.status == "True" { - debug!("Node '{}' is now Ready", node_name); - return Ok(()); - } - } - } - } - } - Err(e) => { - debug!("Failed to get node '{}': {}", node_name, e); - } - } - - sleep(poll_interval).await; - } - } - - /// Sends a single eviction request for `pod`. - async fn evict_pod(&self, pod: &Pod) -> Result<(), Error> { - let name = pod.metadata.name.as_deref().unwrap_or_default(); - let ns = pod.metadata.namespace.as_deref().unwrap_or_default(); - let api: Api = Api::namespaced(self.client.clone(), ns); - debug!("Sending eviction for pod {}/{}", ns, name); - api.evict(name, &EvictParams::default()).await.map(|_| ()) - } - - /// Drains a node by cordoning it, evicting eligible pods, and waiting for - /// them to terminate. - /// - /// The operation mirrors `kubectl drain`: - /// 1. **Cordon** — marks the node as unschedulable. - /// 2. **Classify** — separates pods into evictable / skipped / blocking. - /// 3. **Evict & wait** — sends eviction requests and re-tries on each - /// polling interval until every pod is gone or the timeout expires. - /// - /// Re-sending eviction requests each iteration ensures that pods - /// previously blocked by a `PodDisruptionBudget` are retried once budget - /// becomes available. - /// - /// # Errors - /// Returns an error if the node cannot be cordoned, if any pod blocks - /// eviction (see [`DrainOptions`]), or if evictions do not complete within - /// the configured timeout. - pub async fn drain_node(&self, node_name: &str, options: &DrainOptions) -> Result<(), Error> { - // ── 1. Cordon ────────────────────────────────────────────────── - debug!("Cordoning node '{}'", node_name); - self.cordon_node(node_name).await?; - - // ── 2. List & classify pods ──────────────────────────────────── - let pods = self.list_pods_on_node(node_name).await?; - debug!("Found {} pod(s) on node '{}'", pods.len(), node_name); - - let (evictable, skipped) = - Self::classify_pods_for_drain(&pods, options).map_err(|msg| { - error!("{}", msg); - Error::Discovery(DiscoveryError::MissingResource(msg)) - })?; - - for s in &skipped { - info!("Skipping pod: {}", s); - } - - if evictable.is_empty() { - info!("No pods to evict on node '{}'", node_name); - return Ok(()); - } - - info!( - "Evicting {} pod(s) from node '{}'", - evictable.len(), - node_name - ); - - // ── 3. Evict & wait loop ────────────────────────────────────── - let mut start = tokio::time::Instant::now(); - let poll_interval = Duration::from_secs(5); - let mut pending = evictable; - - loop { - // Send (or re-send) eviction requests for all pending pods. - for pod in &pending { - match self.evict_pod(pod).await { - Ok(()) => {} - // Pod already gone — will be filtered out below. - Err(Error::Api(ErrorResponse { code: 404, .. })) => {} - // PDB is blocking — will retry next iteration. - Err(Error::Api(ErrorResponse { code: 429, .. })) => { - warn!( - "PDB prevented eviction of {}/{}; will retry", - pod.metadata.namespace.as_deref().unwrap_or(""), - pod.metadata.name.as_deref().unwrap_or("") - ); - } - Err(e) => { - error!( - "Failed to evict pod {}/{}: {}", - pod.metadata.namespace.as_deref().unwrap_or(""), - pod.metadata.name.as_deref().unwrap_or(""), - e - ); - return Err(e); - } - } - } - - // Wait before polling pod presence. - sleep(poll_interval).await; - - // Check which pods are still present on the API server. - let mut still_present: Vec = Vec::new(); - for pod in pending { - let ns = pod.metadata.namespace.as_deref().unwrap_or_default(); - let name = pod.metadata.name.as_deref().unwrap_or_default(); - match self.get_pod(name, Some(ns)).await? { - Some(_) => still_present.push(pod), - None => debug!("Pod {}/{} evicted successfully", ns, name), - } - } - - pending = still_present; - - if pending.is_empty() { - break; - } - - if start.elapsed() > options.timeout { - let names: Vec = pending - .iter() - .map(|p| { - format!( - "{}/{}", - p.metadata.namespace.as_deref().unwrap_or(""), - p.metadata.name.as_deref().unwrap_or("") - ) - }) - .collect(); - let msg = format!( - "Timed out after {:?} waiting for pod evictions on node '{}'. Remaining:\n - {}", - options.timeout, - node_name, - names.join("\n - ") - ); - - warn!("{}", msg); - - // Prompt user for action - match helper::prompt_drain_timeout_action( - node_name, - pending.len(), - options.timeout, - )? { - helper::DrainTimeoutAction::Accept => { - // User confirmed acceptance - break the loop and continue - break; - } - helper::DrainTimeoutAction::Retry => { - // Reset the start time to retry for another full timeout period - start = tokio::time::Instant::now(); - continue; - } - helper::DrainTimeoutAction::Abort => { - return Err(Error::Discovery(DiscoveryError::MissingResource(format!( - "Drain operation aborted. {} pods remaining on node '{}'", - pending.len(), - node_name - )))); - } - } - } - - debug!( - "Waiting for {} pod(s) to terminate on node '{}'", - pending.len(), - node_name - ); - } - - debug!("Node '{}' drained successfully", node_name); - Ok(()) - } -} - -fn get_dynamic_api( - resource: ApiResource, - capabilities: ApiCapabilities, - client: Client, - ns: Option<&str>, - all: bool, -) -> Api { - if capabilities.scope == Scope::Cluster || all { - Api::all_with(client, &resource) - } else if let Some(namespace) = ns { - Api::namespaced_with(client, namespace, &resource) - } else { - Api::default_namespaced_with(client, &resource) - } -} - -fn multidoc_deserialize(data: &str) -> Result, serde_yaml::Error> { - use serde::Deserialize; - let mut docs = vec![]; - for de in serde_yaml::Deserializer::from_str(data) { - docs.push(serde_yaml::Value::deserialize(de)?); - } - Ok(docs) -} - -pub trait ApplyStrategy { - fn get_api(client: &Client, ns: Option<&str>) -> Api; -} - -/// Implementation for all resources that are cluster-scoped. -/// It will always use `Api::all` and ignore the namespace parameter. -impl ApplyStrategy for ClusterResourceScope -where - K: Resource, - ::DynamicType: Default, -{ - fn get_api(client: &Client, _ns: Option<&str>) -> Api { - Api::all(client.clone()) - } -} - -/// Implementation for all resources that are namespace-scoped. -/// It will use `Api::namespaced` if a namespace is provided, otherwise -/// it falls back to the default namespace configured in your kubeconfig. -impl ApplyStrategy for NamespaceResourceScope -where - K: Resource, - ::DynamicType: Default, -{ - fn get_api(client: &Client, ns: Option<&str>) -> Api { - match ns { - Some(ns) => Api::namespaced(client.clone(), ns), - None => Api::default_namespaced(client.clone()), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use k8s_openapi::api::core::v1::{EmptyDirVolumeSource, Pod, PodSpec, PodStatus, Volume}; - use k8s_openapi::apimachinery::pkg::apis::meta::v1::{ObjectMeta, OwnerReference}; - use std::collections::BTreeMap; - - // ── Test helpers ──────────────────────────────────────────────────── - - /// Builds a minimal pod with the given name/namespace and no special - /// annotations, owner refs, volumes, or status. - fn base_pod(name: &str, ns: &str) -> Pod { - Pod { - metadata: ObjectMeta { - name: Some(name.to_string()), - namespace: Some(ns.to_string()), - ..Default::default() - }, - spec: Some(PodSpec::default()), - status: Some(PodStatus { - phase: Some("Running".to_string()), - ..Default::default() - }), - } - } - - fn mirror_pod(name: &str, ns: &str) -> Pod { - let mut pod = base_pod(name, ns); - let mut annotations = BTreeMap::new(); - annotations.insert( - "kubernetes.io/config.mirror".to_string(), - "abc123".to_string(), - ); - pod.metadata.annotations = Some(annotations); - pod - } - - fn daemonset_pod(name: &str, ns: &str) -> Pod { - let mut pod = base_pod(name, ns); - pod.metadata.owner_references = Some(vec![OwnerReference { - api_version: "apps/v1".to_string(), - kind: "DaemonSet".to_string(), - name: "some-ds".to_string(), - uid: "uid-ds".to_string(), - ..Default::default() - }]); - pod - } - - fn emptydir_pod(name: &str, ns: &str) -> Pod { - let mut pod = base_pod(name, ns); - pod.spec = Some(PodSpec { - volumes: Some(vec![Volume { - name: "scratch".to_string(), - empty_dir: Some(EmptyDirVolumeSource::default()), - ..Default::default() - }]), - ..Default::default() - }); - pod - } - - fn completed_pod(name: &str, ns: &str, phase: &str) -> Pod { - let mut pod = base_pod(name, ns); - pod.status = Some(PodStatus { - phase: Some(phase.to_string()), - ..Default::default() - }); - pod - } - - fn default_opts() -> DrainOptions { - DrainOptions::default() - } - - // ── Tests ─────────────────────────────────────────────────────────── - - #[test] - fn empty_pod_list_returns_empty_vecs() { - let (evictable, skipped) = - K8sClient::classify_pods_for_drain(&[], &default_opts()).unwrap(); - assert!(evictable.is_empty()); - assert!(skipped.is_empty()); - } - - #[test] - fn normal_pod_is_evictable() { - let pods = vec![base_pod("web", "default")]; - let (evictable, skipped) = - K8sClient::classify_pods_for_drain(&pods, &default_opts()).unwrap(); - assert_eq!(evictable.len(), 1); - assert_eq!(evictable[0].metadata.name.as_deref(), Some("web")); - assert!(skipped.is_empty()); - } - - #[test] - fn mirror_pod_is_skipped() { - let pods = vec![mirror_pod("kube-apiserver", "kube-system")]; - let (evictable, skipped) = - K8sClient::classify_pods_for_drain(&pods, &default_opts()).unwrap(); - assert!(evictable.is_empty()); - assert_eq!(skipped.len(), 1); - assert!(skipped[0].contains("mirror pod")); - } - - #[test] - fn completed_succeeded_pod_is_skipped() { - let pods = vec![completed_pod("job-xyz", "batch", "Succeeded")]; - let (evictable, skipped) = - K8sClient::classify_pods_for_drain(&pods, &default_opts()).unwrap(); - assert!(evictable.is_empty()); - assert_eq!(skipped.len(), 1); - assert!(skipped[0].contains("completed")); - } - - #[test] - fn completed_failed_pod_is_skipped() { - let pods = vec![completed_pod("job-fail", "batch", "Failed")]; - let (evictable, skipped) = - K8sClient::classify_pods_for_drain(&pods, &default_opts()).unwrap(); - assert!(evictable.is_empty()); - assert_eq!(skipped.len(), 1); - assert!(skipped[0].contains("completed")); - } - - #[test] - fn daemonset_pod_skipped_when_ignore_daemonsets_true() { - let pods = vec![daemonset_pod("fluentd", "logging")]; - let opts = DrainOptions { - ignore_daemonsets: true, - ..default_opts() - }; - let (evictable, skipped) = K8sClient::classify_pods_for_drain(&pods, &opts).unwrap(); - assert!(evictable.is_empty()); - assert_eq!(skipped.len(), 1); - assert!(skipped[0].contains("DaemonSet-managed")); - } - - #[test] - fn daemonset_pod_blocks_when_ignore_daemonsets_false() { - let pods = vec![daemonset_pod("fluentd", "logging")]; - let opts = DrainOptions { - ignore_daemonsets: false, - ..default_opts() - }; - let err = K8sClient::classify_pods_for_drain(&pods, &opts).unwrap_err(); - assert!(err.contains("DaemonSet")); - assert!(err.contains("logging/fluentd")); - } - - #[test] - fn emptydir_pod_blocks_when_delete_emptydir_data_false() { - let pods = vec![emptydir_pod("cache", "default")]; - let opts = DrainOptions { - delete_emptydir_data: false, - ..default_opts() - }; - let err = K8sClient::classify_pods_for_drain(&pods, &opts).unwrap_err(); - assert!(err.contains("emptyDir")); - assert!(err.contains("default/cache")); - } - - #[test] - fn emptydir_pod_evictable_when_delete_emptydir_data_true() { - let pods = vec![emptydir_pod("cache", "default")]; - let opts = DrainOptions { - delete_emptydir_data: true, - ..default_opts() - }; - let (evictable, skipped) = K8sClient::classify_pods_for_drain(&pods, &opts).unwrap(); - assert_eq!(evictable.len(), 1); - assert_eq!(evictable[0].metadata.name.as_deref(), Some("cache")); - assert!(skipped.is_empty()); - } - - #[test] - fn multiple_blocking_pods_all_reported() { - let pods = vec![daemonset_pod("ds-a", "ns1"), emptydir_pod("ed-b", "ns2")]; - let opts = DrainOptions { - ignore_daemonsets: false, - delete_emptydir_data: false, - ..default_opts() - }; - let err = K8sClient::classify_pods_for_drain(&pods, &opts).unwrap_err(); - assert!(err.contains("ns1/ds-a")); - assert!(err.contains("ns2/ed-b")); - } - - #[test] - fn mixed_pods_classified_correctly() { - let pods = vec![ - base_pod("web", "default"), - mirror_pod("kube-apiserver", "kube-system"), - daemonset_pod("fluentd", "logging"), - completed_pod("job-done", "batch", "Succeeded"), - base_pod("api", "default"), - ]; - let (evictable, skipped) = - K8sClient::classify_pods_for_drain(&pods, &default_opts()).unwrap(); - - let evict_names: Vec<&str> = evictable - .iter() - .map(|p| p.metadata.name.as_deref().unwrap()) - .collect(); - assert_eq!(evict_names, vec!["web", "api"]); - assert_eq!(skipped.len(), 3); - } - - #[test] - fn classification_priority_mirror_before_completed() { - // A mirror pod that also has phase=Succeeded should still be - // classified as "mirror pod" (the first check wins). - let mut pod = mirror_pod("static-etcd", "kube-system"); - pod.status = Some(PodStatus { - phase: Some("Succeeded".to_string()), - ..Default::default() - }); - let (evictable, skipped) = - K8sClient::classify_pods_for_drain(&[pod], &default_opts()).unwrap(); - assert!(evictable.is_empty()); - assert_eq!(skipped.len(), 1); - assert!( - skipped[0].contains("mirror pod"), - "expected mirror-pod label, got: {}", - skipped[0] - ); - } - - #[test] - fn classification_priority_completed_before_daemonset() { - // A completed DaemonSet pod should be skipped as "completed", - // not as "DaemonSet-managed". - let mut pod = daemonset_pod("collector", "monitoring"); - pod.status = Some(PodStatus { - phase: Some("Failed".to_string()), - ..Default::default() - }); - let (evictable, skipped) = - K8sClient::classify_pods_for_drain(&[pod], &default_opts()).unwrap(); - assert!(evictable.is_empty()); - assert_eq!(skipped.len(), 1); - assert!( - skipped[0].contains("completed"), - "expected completed label, got: {}", - skipped[0] - ); - } - - #[test] - fn pod_with_no_metadata_names_uses_unknown_placeholder() { - let pod = Pod { - metadata: ObjectMeta::default(), - spec: Some(PodSpec::default()), - status: Some(PodStatus { - phase: Some("Running".to_string()), - ..Default::default() - }), - }; - let (evictable, _) = K8sClient::classify_pods_for_drain(&[pod], &default_opts()).unwrap(); - assert_eq!(evictable.len(), 1); - } -} - -#[cfg(test)] -mod apply_tests { - //! Integration tests for apply() and apply_dynamic() functions. - //! - //! ## Testing Strategy - //! - //! These functions interact with the Kubernetes API server, making them difficult - //! to unit test. We recommend a multi-layered testing approach: - //! - //! ### 1. **Integration Tests with Real Cluster (Recommended)** - //! - Use a local development cluster (kind, k3d, minikube) - //! - Place tests in `tests/` directory for optional execution - //! - Run with: `cargo test --test k8s_apply_integration -- --ignored` - //! - //! ### 2. **Contract Tests with Mock Server** - //! - Use `wiremock` or `mockito` to simulate Kubernetes API responses - //! - Test specific scenarios: 404 → create, 200 → update, error cases - //! - Fast, deterministic, no cluster required - //! - //! ### 3. **Property-Based Tests** - //! - Use `proptest` to generate various resource configurations - //! - Verify idempotency: apply(x) → apply(x) should not error - //! - //! ### 4. **Example Tests Below** - //! - These demonstrate the testing patterns - //! - Marked with `#[ignore]` to require opt-in execution - //! - Can be run in CI with proper cluster setup - //! - //! ## Running Tests - //! - //! ```bash - //! # Setup test cluster - //! kind create cluster --name harmony-test - //! - //! # Run integration tests - //! cargo test --test k8s_apply_integration - //! - //! # Or run ignored tests in this module - //! cargo test apply_tests -- --ignored --nocapture - //! ``` - - use kube::api::TypeMeta; - - use super::*; - - /// Example integration test for apply() with ConfigMap creation. - /// - /// This test requires a real Kubernetes cluster and is marked as ignored. - /// Run with: `cargo test apply_creates_new_configmap -- --ignored` - #[tokio::test] - #[ignore = "requires kubernetes cluster"] - async fn apply_creates_new_configmap() { - let client = K8sClient::try_default() - .await - .expect("failed to create client"); - let test_ns = "default"; - let cm_name = format!( - "test-cm-{}", - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() - ); - - let mut data = BTreeMap::new(); - data.insert("key1".to_string(), "value1".to_string()); - - let configmap = ConfigMap { - metadata: ObjectMeta { - name: Some(cm_name.clone()), - namespace: Some(test_ns.to_string()), - ..Default::default() - }, - data: Some(data), - ..Default::default() - }; - - // Apply should create the resource - let result = client.apply(&configmap, Some(test_ns)).await; - assert!( - result.is_ok(), - "failed to apply new ConfigMap: {:?}", - result.err() - ); - - // Verify it exists - let fetched: Option = - client.get_resource(&cm_name, Some(test_ns)).await.unwrap(); - assert!(fetched.is_some(), "ConfigMap was not created"); - assert_eq!( - fetched.unwrap().data.unwrap().get("key1").unwrap(), - "value1" - ); - - // Cleanup - let api: Api = Api::namespaced(client.client.clone(), test_ns); - let _ = api.delete(&cm_name, &DeleteParams::default()).await; - } - - /// Example integration test for apply() updating an existing resource. - #[tokio::test] - #[ignore = "requires kubernetes cluster"] - async fn apply_updates_existing_configmap() { - let client = K8sClient::try_default() - .await - .expect("failed to create client"); - let test_ns = "default"; - let cm_name = format!( - "test-cm-{}", - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() - ); - - // Create initial ConfigMap - let mut data = BTreeMap::new(); - data.insert("key1".to_string(), "value1".to_string()); - let configmap = ConfigMap { - metadata: ObjectMeta { - name: Some(cm_name.clone()), - namespace: Some(test_ns.to_string()), - ..Default::default() - }, - data: Some(data.clone()), - ..Default::default() - }; - - client.apply(&configmap, Some(test_ns)).await.unwrap(); - - // Update the ConfigMap - data.insert("key2".to_string(), "value2".to_string()); - let updated_cm = ConfigMap { - metadata: ObjectMeta { - name: Some(cm_name.clone()), - namespace: Some(test_ns.to_string()), - ..Default::default() - }, - data: Some(data), - ..Default::default() - }; - - let result = client.apply(&updated_cm, Some(test_ns)).await; - assert!( - result.is_ok(), - "failed to update ConfigMap: {:?}", - result.err() - ); - - // Verify the update - let fetched: Option = - client.get_resource(&cm_name, Some(test_ns)).await.unwrap(); - let fetched_data = fetched.unwrap().data.unwrap(); - assert_eq!(fetched_data.get("key1").unwrap(), "value1"); - assert_eq!(fetched_data.get("key2").unwrap(), "value2"); - - // Cleanup - let api: Api = Api::namespaced(client.client.clone(), test_ns); - let _ = api.delete(&cm_name, &DeleteParams::default()).await; - } - - /// Example integration test for apply_dynamic() with new resource. - #[tokio::test] - #[ignore = "requires kubernetes cluster"] - async fn apply_dynamic_creates_new_resource() { - let client = K8sClient::try_default() - .await - .expect("failed to create client"); - let test_ns = "default"; - let cm_name = format!( - "test-dyn-cm-{}", - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() - ); - - let mut data = BTreeMap::new(); - data.insert("foo".to_string(), serde_json::json!("bar")); - - let dynamic_obj = DynamicObject { - types: Some(TypeMeta { - api_version: "v1".to_string(), - kind: "ConfigMap".to_string(), - }), - metadata: ObjectMeta { - name: Some(cm_name.clone()), - namespace: Some(test_ns.to_string()), - ..Default::default() - }, - data: serde_json::json!(data), - }; - - let result = client - .apply_dynamic(&dynamic_obj, Some(test_ns), false) - .await; - assert!( - result.is_ok(), - "failed to apply dynamic object: {:?}", - result.err() - ); - - // Verify it exists - let api: Api = Api::namespaced(client.client.clone(), test_ns); - let fetched = api.get_opt(&cm_name).await.unwrap(); - assert!(fetched.is_some(), "Dynamic resource was not created"); - - // Cleanup - let _ = api.delete(&cm_name, &DeleteParams::default()).await; - } - - /// Example showing idempotency: applying same resource twice should succeed. - #[tokio::test] - #[ignore = "requires kubernetes cluster"] - async fn apply_is_idempotent() { - let client = K8sClient::try_default() - .await - .expect("failed to create client"); - let test_ns = "default"; - let cm_name = format!( - "test-idem-{}", - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() - ); - - let configmap = ConfigMap { - metadata: ObjectMeta { - name: Some(cm_name.clone()), - namespace: Some(test_ns.to_string()), - ..Default::default() - }, - data: Some(BTreeMap::from([("key".to_string(), "value".to_string())])), - ..Default::default() - }; - - // Apply twice - let result1 = client.apply(&configmap, Some(test_ns)).await; - let result2 = client.apply(&configmap, Some(test_ns)).await; - - assert!(result1.is_ok(), "first apply failed"); - assert!(result2.is_ok(), "second apply failed (not idempotent)"); - - // Cleanup - let api: Api = Api::namespaced(client.client.clone(), test_ns); - let _ = api.delete(&cm_name, &DeleteParams::default()).await; - } -} diff --git a/harmony/src/domain/topology/k8s_anywhere/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere/k8s_anywhere.rs index 55091d2..0f93a18 100644 --- a/harmony/src/domain/topology/k8s_anywhere/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere/k8s_anywhere.rs @@ -2,6 +2,7 @@ use std::{collections::BTreeMap, process::Command, sync::Arc, time::Duration}; use async_trait::async_trait; use base64::{Engine, engine::general_purpose}; +use harmony_k8s::{K8sClient, KubernetesDistribution}; use harmony_types::rfc1123::Rfc1123Name; use k8s_openapi::api::{ core::v1::{Pod, Secret}, @@ -58,7 +59,6 @@ use crate::{ use super::super::{ DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, PreparationError, PreparationOutcome, Topology, - k8s::K8sClient, oberservability::monitoring::AlertReceiver, tenant::{ TenantConfig, TenantManager, @@ -76,13 +76,6 @@ struct K8sState { message: String, } -#[derive(Debug, Clone, Serialize)] -pub enum KubernetesDistribution { - OpenshiftFamily, - K3sFamily, - Default, -} - #[derive(Debug, Clone)] enum K8sSource { LocalK3d, diff --git a/harmony/src/domain/topology/k8s_anywhere/postgres.rs b/harmony/src/domain/topology/k8s_anywhere/postgres.rs index 2bf800b..76356c6 100644 --- a/harmony/src/domain/topology/k8s_anywhere/postgres.rs +++ b/harmony/src/domain/topology/k8s_anywhere/postgres.rs @@ -1,7 +1,6 @@ use async_trait::async_trait; use crate::{ - interpret::Outcome, inventory::Inventory, modules::postgresql::{ K8sPostgreSQLScore, diff --git a/harmony/src/domain/topology/mod.rs b/harmony/src/domain/topology/mod.rs index 28161fc..c661e8a 100644 --- a/harmony/src/domain/topology/mod.rs +++ b/harmony/src/domain/topology/mod.rs @@ -16,7 +16,6 @@ pub mod tenant; use derive_new::new; pub use k8s_anywhere::*; pub use localhost::*; -pub mod k8s; mod load_balancer; pub mod router; mod tftp; diff --git a/harmony/src/domain/topology/network.rs b/harmony/src/domain/topology/network.rs index 1ec3cf3..b80b3e0 100644 --- a/harmony/src/domain/topology/network.rs +++ b/harmony/src/domain/topology/network.rs @@ -9,6 +9,7 @@ use std::{ use async_trait::async_trait; use brocade::PortOperatingMode; use derive_new::new; +use harmony_k8s::K8sClient; use harmony_types::{ id::Id, net::{IpAddress, MacAddress}, @@ -18,7 +19,7 @@ use serde::Serialize; use crate::executors::ExecutorError; -use super::{LogicalHost, k8s::K8sClient}; +use super::LogicalHost; #[derive(Debug)] pub struct DHCPStaticEntry { diff --git a/harmony/src/domain/topology/tenant/k8s.rs b/harmony/src/domain/topology/tenant/k8s.rs index d7d99c0..b4c8b17 100644 --- a/harmony/src/domain/topology/tenant/k8s.rs +++ b/harmony/src/domain/topology/tenant/k8s.rs @@ -1,10 +1,8 @@ use std::sync::Arc; -use crate::{ - executors::ExecutorError, - topology::k8s::{ApplyStrategy, K8sClient}, -}; +use crate::executors::ExecutorError; use async_trait::async_trait; +use harmony_k8s::K8sClient; use k8s_openapi::{ api::{ core::v1::{LimitRange, Namespace, ResourceQuota}, @@ -14,7 +12,7 @@ use k8s_openapi::{ }, apimachinery::pkg::util::intstr::IntOrString, }; -use kube::{Resource, api::DynamicObject}; +use kube::Resource; use log::debug; use serde::de::DeserializeOwned; use serde_json::json; @@ -59,7 +57,6 @@ impl K8sTenantManager { ) -> Result where ::DynamicType: Default, - ::Scope: ApplyStrategy, { self.apply_labels(&mut resource, config); self.k8s_client diff --git a/harmony/src/infra/network_manager.rs b/harmony/src/infra/network_manager.rs index 6b7a342..8323eba 100644 --- a/harmony/src/infra/network_manager.rs +++ b/harmony/src/infra/network_manager.rs @@ -5,6 +5,7 @@ use std::{ use askama::Template; use async_trait::async_trait; +use harmony_k8s::{DrainOptions, K8sClient, NodeFile}; use harmony_types::id::Id; use k8s_openapi::api::core::v1::Node; use kube::{ @@ -15,10 +16,7 @@ use log::{debug, info, warn}; use crate::{ modules::okd::crd::nmstate, - topology::{ - HostNetworkConfig, NetworkError, NetworkManager, - k8s::{DrainOptions, K8sClient, NodeFile}, - }, + topology::{HostNetworkConfig, NetworkError, NetworkManager}, }; /// NetworkManager bond configuration template diff --git a/harmony/src/modules/application/backend_app.rs b/harmony/src/modules/application/backend_app.rs index d11feaa..4183e30 100644 --- a/harmony/src/modules/application/backend_app.rs +++ b/harmony/src/modules/application/backend_app.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use log::{debug, info, trace}; +use log::{debug, info}; use serde::Serialize; use std::path::PathBuf; diff --git a/harmony/src/modules/application/features/helm_argocd_score.rs b/harmony/src/modules/application/features/helm_argocd_score.rs index 4a65a1e..6f02a22 100644 --- a/harmony/src/modules/application/features/helm_argocd_score.rs +++ b/harmony/src/modules/application/features/helm_argocd_score.rs @@ -1,4 +1,5 @@ use async_trait::async_trait; +use harmony_k8s::K8sClient; use harmony_macros::hurl; use log::{debug, info, trace, warn}; use non_blank_string_rs::NonBlankString; @@ -14,7 +15,7 @@ use crate::{ helm::chart::{HelmChartScore, HelmRepository}, }, score::Score, - topology::{HelmCommand, K8sclient, Topology, ingress::Ingress, k8s::K8sClient}, + topology::{HelmCommand, K8sclient, Topology, ingress::Ingress}, }; use harmony_types::id::Id; diff --git a/harmony/src/modules/argocd/mod.rs b/harmony/src/modules/argocd/mod.rs index 402c90f..4f4329d 100644 --- a/harmony/src/modules/argocd/mod.rs +++ b/harmony/src/modules/argocd/mod.rs @@ -1,8 +1,9 @@ use std::sync::Arc; +use harmony_k8s::K8sClient; use log::{debug, info}; -use crate::{interpret::InterpretError, topology::k8s::K8sClient}; +use crate::interpret::InterpretError; #[derive(Clone, Debug, PartialEq, Eq)] pub enum ArgoScope { diff --git a/harmony/src/modules/cert_manager/cluster_issuer.rs b/harmony/src/modules/cert_manager/cluster_issuer.rs index 70294fe..38f9e6b 100644 --- a/harmony/src/modules/cert_manager/cluster_issuer.rs +++ b/harmony/src/modules/cert_manager/cluster_issuer.rs @@ -1,3 +1,4 @@ +use harmony_k8s::K8sClient; use std::sync::Arc; use async_trait::async_trait; @@ -11,7 +12,7 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::{K8sclient, Topology, k8s::K8sClient}, + topology::{K8sclient, Topology}, }; #[derive(Clone, Debug, Serialize)] diff --git a/harmony/src/modules/k8s/failover.rs b/harmony/src/modules/k8s/failover.rs index 939d9ab..b660e07 100644 --- a/harmony/src/modules/k8s/failover.rs +++ b/harmony/src/modules/k8s/failover.rs @@ -3,7 +3,8 @@ use std::sync::Arc; use async_trait::async_trait; use log::warn; -use crate::topology::{FailoverTopology, K8sclient, k8s::K8sClient}; +use crate::topology::{FailoverTopology, K8sclient}; +use harmony_k8s::K8sClient; #[async_trait] impl K8sclient for FailoverTopology { diff --git a/harmony/src/modules/k8s/resource.rs b/harmony/src/modules/k8s/resource.rs index 83e10a7..bff8183 100644 --- a/harmony/src/modules/k8s/resource.rs +++ b/harmony/src/modules/k8s/resource.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use k8s_openapi::{NamespaceResourceScope, ResourceScope}; +use k8s_openapi::ResourceScope; use kube::Resource; use log::info; use serde::{Serialize, de::DeserializeOwned}; @@ -109,7 +109,7 @@ where topology .k8s_client() .await - .expect("Environment should provide enough information to instanciate a client") + .map_err(|e| InterpretError::new(format!("Failed to get k8s client : {e}")))? .apply_many(&self.score.resource, self.score.namespace.as_deref()) .await?; diff --git a/harmony/src/modules/monitoring/kube_prometheus/crd/crd_alertmanager_config.rs b/harmony/src/modules/monitoring/kube_prometheus/crd/crd_alertmanager_config.rs index 88ec745..a547dce 100644 --- a/harmony/src/modules/monitoring/kube_prometheus/crd/crd_alertmanager_config.rs +++ b/harmony/src/modules/monitoring/kube_prometheus/crd/crd_alertmanager_config.rs @@ -6,7 +6,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::{ - interpret::{InterpretError, Outcome}, + interpret::InterpretError, inventory::Inventory, modules::{ monitoring::{ @@ -17,10 +17,10 @@ use crate::{ topology::{ K8sclient, Topology, installable::Installable, - k8s::K8sClient, oberservability::monitoring::{AlertReceiver, AlertSender, ScrapeTarget}, }, }; +use harmony_k8s::K8sClient; #[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)] #[kube( diff --git a/harmony/src/modules/monitoring/kube_prometheus/crd/rhob_alertmanager_config.rs b/harmony/src/modules/monitoring/kube_prometheus/crd/rhob_alertmanager_config.rs index a53b24e..4e6e68e 100644 --- a/harmony/src/modules/monitoring/kube_prometheus/crd/rhob_alertmanager_config.rs +++ b/harmony/src/modules/monitoring/kube_prometheus/crd/rhob_alertmanager_config.rs @@ -4,10 +4,8 @@ use kube::CustomResource; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::topology::{ - k8s::K8sClient, - oberservability::monitoring::{AlertReceiver, AlertSender}, -}; +use crate::topology::oberservability::monitoring::{AlertReceiver, AlertSender}; +use harmony_k8s::K8sClient; #[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)] #[kube( diff --git a/harmony/src/modules/monitoring/ntfy/ntfy.rs b/harmony/src/modules/monitoring/ntfy/ntfy.rs index f82aaf7..9b796d9 100644 --- a/harmony/src/modules/monitoring/ntfy/ntfy.rs +++ b/harmony/src/modules/monitoring/ntfy/ntfy.rs @@ -11,8 +11,9 @@ use crate::{ inventory::Inventory, modules::monitoring::ntfy::helm::ntfy_helm_chart::ntfy_helm_chart_score, score::Score, - topology::{HelmCommand, K8sclient, MultiTargetTopology, Topology, k8s::K8sClient}, + topology::{HelmCommand, K8sclient, MultiTargetTopology, Topology}, }; +use harmony_k8s::K8sClient; use harmony_types::id::Id; #[derive(Debug, Clone, Serialize)] diff --git a/harmony/src/modules/monitoring/okd/config.rs b/harmony/src/modules/monitoring/okd/config.rs index b86c5f0..4e53eb8 100644 --- a/harmony/src/modules/monitoring/okd/config.rs +++ b/harmony/src/modules/monitoring/okd/config.rs @@ -1,9 +1,7 @@ use std::{collections::BTreeMap, sync::Arc}; -use crate::{ - interpret::{InterpretError, Outcome}, - topology::k8s::K8sClient, -}; +use crate::interpret::{InterpretError, Outcome}; +use harmony_k8s::K8sClient; use k8s_openapi::api::core::v1::ConfigMap; use kube::api::ObjectMeta; diff --git a/harmony/src/modules/nats/score_nats_k8s.rs b/harmony/src/modules/nats/score_nats_k8s.rs index cad35e3..4aac85f 100644 --- a/harmony/src/modules/nats/score_nats_k8s.rs +++ b/harmony/src/modules/nats/score_nats_k8s.rs @@ -1,6 +1,7 @@ use std::{collections::BTreeMap, str::FromStr}; use async_trait::async_trait; +use harmony_k8s::KubernetesDistribution; use harmony_macros::hurl; use harmony_secret::{Secret, SecretManager}; use harmony_types::id::Id; @@ -25,7 +26,7 @@ use crate::{ }, }, score::Score, - topology::{HelmCommand, K8sclient, KubernetesDistribution, TlsRouter, Topology}, + topology::{HelmCommand, K8sclient, TlsRouter, Topology}, }; #[derive(Debug, Clone, Serialize)] diff --git a/harmony/src/modules/postgresql/cnpg/crd.rs b/harmony/src/modules/postgresql/cnpg/crd.rs index c8f6126..5b54fd0 100644 --- a/harmony/src/modules/postgresql/cnpg/crd.rs +++ b/harmony/src/modules/postgresql/cnpg/crd.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use kube::{CustomResource, api::ObjectMeta}; use serde::{Deserialize, Serialize}; @@ -13,9 +15,14 @@ use serde::{Deserialize, Serialize}; #[serde(rename_all = "camelCase")] pub struct ClusterSpec { pub instances: u32, + #[serde(skip_serializing_if = "Option::is_none")] pub image_name: Option, pub storage: Storage, pub bootstrap: Bootstrap, + /// This must be set to None if you want cnpg to generate a superuser secret + #[serde(skip_serializing_if = "Option::is_none")] + pub superuser_secret: Option>, + pub enable_superuser_access: bool, } impl Default for Cluster { @@ -34,6 +41,8 @@ impl Default for ClusterSpec { image_name: None, storage: Storage::default(), bootstrap: Bootstrap::default(), + superuser_secret: None, + enable_superuser_access: false, } } } diff --git a/harmony/src/modules/postgresql/operator.rs b/harmony/src/modules/postgresql/operator.rs index d908361..1eb2af9 100644 --- a/harmony/src/modules/postgresql/operator.rs +++ b/harmony/src/modules/postgresql/operator.rs @@ -20,7 +20,7 @@ use crate::topology::{K8sclient, Topology}; /// # Usage /// ``` /// use harmony::modules::postgresql::CloudNativePgOperatorScore; -/// let score = CloudNativePgOperatorScore::default(); +/// let score = CloudNativePgOperatorScore::default_openshift(); /// ``` /// /// Or, you can take control of most relevant fiedls this way : @@ -52,8 +52,8 @@ pub struct CloudNativePgOperatorScore { pub source_namespace: String, } -impl Default for CloudNativePgOperatorScore { - fn default() -> Self { +impl CloudNativePgOperatorScore { + pub fn default_openshift() -> Self { Self { namespace: "openshift-operators".to_string(), channel: "stable-v1".to_string(), @@ -68,7 +68,7 @@ impl CloudNativePgOperatorScore { pub fn new(namespace: &str) -> Self { Self { namespace: namespace.to_string(), - ..Default::default() + ..Self::default_openshift() } } } diff --git a/harmony/src/modules/postgresql/score_k8s.rs b/harmony/src/modules/postgresql/score_k8s.rs index 5e3cb08..5c4b834 100644 --- a/harmony/src/modules/postgresql/score_k8s.rs +++ b/harmony/src/modules/postgresql/score_k8s.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use serde::Serialize; use crate::interpret::Interpret; @@ -66,6 +68,11 @@ impl Score for K8sPostgreSQLScore { owner: "app".to_string(), }, }, + // superuser_secret: Some(BTreeMap::from([( + // "name".to_string(), + // format!("{}-superuser", self.config.cluster_name.clone()), + // )])), + enable_superuser_access: true, ..ClusterSpec::default() }; diff --git a/harmony/src/modules/prometheus/k8s_prometheus_alerting_score.rs b/harmony/src/modules/prometheus/k8s_prometheus_alerting_score.rs index 7093ee8..586029b 100644 --- a/harmony/src/modules/prometheus/k8s_prometheus_alerting_score.rs +++ b/harmony/src/modules/prometheus/k8s_prometheus_alerting_score.rs @@ -12,8 +12,7 @@ use crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::C use crate::modules::monitoring::kube_prometheus::crd::crd_default_rules::build_default_application_rules; use crate::modules::monitoring::kube_prometheus::crd::crd_grafana::{ Grafana, GrafanaDashboard, GrafanaDashboardSpec, GrafanaDatasource, GrafanaDatasourceConfig, - GrafanaDatasourceJsonData, GrafanaDatasourceSpec, GrafanaSecretKeyRef, GrafanaSpec, - GrafanaValueFrom, GrafanaValueSource, + GrafanaDatasourceJsonData, GrafanaDatasourceSpec, GrafanaSpec, }; use crate::modules::monitoring::kube_prometheus::crd::crd_prometheus_rules::{ PrometheusRule, PrometheusRuleSpec, RuleGroup, @@ -23,7 +22,7 @@ use crate::modules::monitoring::kube_prometheus::crd::service_monitor::{ ServiceMonitor, ServiceMonitorSpec, }; use crate::topology::oberservability::monitoring::AlertReceiver; -use crate::topology::{K8sclient, Topology, k8s::K8sClient}; +use crate::topology::{K8sclient, Topology}; use crate::{ data::Version, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, @@ -38,6 +37,7 @@ use crate::{ }, score::Score, }; +use harmony_k8s::K8sClient; use harmony_types::id::Id; use super::prometheus::PrometheusMonitoring; diff --git a/harmony/src/modules/prometheus/rhob_alerting_score.rs b/harmony/src/modules/prometheus/rhob_alerting_score.rs index 644e6f9..8a85d1b 100644 --- a/harmony/src/modules/prometheus/rhob_alerting_score.rs +++ b/harmony/src/modules/prometheus/rhob_alerting_score.rs @@ -30,12 +30,13 @@ use crate::modules::monitoring::kube_prometheus::crd::rhob_service_monitor::{ use crate::score::Score; use crate::topology::ingress::Ingress; use crate::topology::oberservability::monitoring::AlertReceiver; -use crate::topology::{K8sclient, Topology, k8s::K8sClient}; +use crate::topology::{K8sclient, Topology}; use crate::{ data::Version, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, }; +use harmony_k8s::K8sClient; use harmony_types::id::Id; use super::prometheus::PrometheusMonitoring; diff --git a/harmony/src/modules/storage/ceph/ceph_remove_osd_score.rs b/harmony/src/modules/storage/ceph/ceph_remove_osd_score.rs index 787f9cc..eca87b0 100644 --- a/harmony/src/modules/storage/ceph/ceph_remove_osd_score.rs +++ b/harmony/src/modules/storage/ceph/ceph_remove_osd_score.rs @@ -4,6 +4,7 @@ use std::{ }; use async_trait::async_trait; +use harmony_k8s::K8sClient; use log::{debug, warn}; use serde::{Deserialize, Serialize}; use tokio::time::sleep; @@ -13,7 +14,7 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::{K8sclient, Topology, k8s::K8sClient}, + topology::{K8sclient, Topology}, }; use harmony_types::id::Id; diff --git a/harmony/src/modules/storage/ceph/ceph_validate_health_score.rs b/harmony/src/modules/storage/ceph/ceph_validate_health_score.rs index ee331bc..067d9f9 100644 --- a/harmony/src/modules/storage/ceph/ceph_validate_health_score.rs +++ b/harmony/src/modules/storage/ceph/ceph_validate_health_score.rs @@ -9,8 +9,9 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::{K8sclient, Topology, k8s::K8sClient}, + topology::{K8sclient, Topology}, }; +use harmony_k8s::K8sClient; use harmony_types::id::Id; #[derive(Clone, Debug, Serialize)] diff --git a/harmony/src/modules/zitadel/mod.rs b/harmony/src/modules/zitadel/mod.rs index 1bd8a05..aebacf8 100644 --- a/harmony/src/modules/zitadel/mod.rs +++ b/harmony/src/modules/zitadel/mod.rs @@ -1,43 +1,476 @@ +use k8s_openapi::api::core::v1::Namespace; +use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; +use k8s_openapi::{ByteString, api::core::v1::Secret}; +use kube::{Error as KubeError, core::ErrorResponse}; +use rand::distr::Distribution; +use rand::{Rng, rng, seq::SliceRandom}; +use std::collections::BTreeMap; use std::str::FromStr; +use async_trait::async_trait; use harmony_macros::hurl; +use harmony_types::id::Id; +use harmony_types::storage::StorageSize; +use log::{debug, error, info, trace, warn}; use non_blank_string_rs::NonBlankString; use serde::Serialize; use crate::{ - interpret::Interpret, + data::Version, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::Inventory, modules::helm::chart::{HelmChartScore, HelmRepository}, + modules::k8s::resource::K8sResourceScore, + modules::postgresql::capability::{PostgreSQL, PostgreSQLClusterRole, PostgreSQLConfig}, score::Score, topology::{HelmCommand, K8sclient, Topology}, }; +const NAMESPACE: &str = "zitadel"; +const PG_CLUSTER_NAME: &str = "zitadel-pg"; +const MASTERKEY_SECRET_NAME: &str = "zitadel-masterkey"; + +/// Opinionated Zitadel deployment score. +/// +/// Deploys a PostgreSQL cluster (via the [`PostgreSQL`] trait) and the Zitadel +/// Helm chart into the same namespace. Intended as a central multi-tenant IdP +/// with SSO for OKD/OpenShift, OpenBao, Harbor, Grafana, Nextcloud, Ente +/// Photos, and others. +/// +/// # Ingress annotations +/// No controller-specific ingress annotations are set by default. On +/// OKD/OpenShift, the ingress should request TLS so the generated Route is +/// edge-terminated instead of HTTP-only. Optional cert-manager annotations are +/// included for clusters that have cert-manager installed; clusters without +/// cert-manager will ignore them. +/// Add or adjust annotations via `values_overrides` depending on your +/// distribution: +/// - NGINX: `nginx.ingress.kubernetes.io/backend-protocol: GRPC` +/// - OpenShift HAProxy: `route.openshift.io/termination: edge` +/// - AWS ALB: set `ingress.controller: aws` + +/// +/// # Database credentials +/// CNPG creates a `-superuser` secret with key `password`. Because +/// `envVarsSecret` injects secret keys verbatim as env var names and the CNPG +/// key (`password`) does not match ZITADEL's expected name +/// (`ZITADEL_DATABASE_POSTGRES_USER_PASSWORD`), individual `env` entries with +/// `valueFrom.secretKeyRef` are used instead. For environments with an +/// External Secrets Operator or similar, create a dedicated secret with the +/// correct ZITADEL env var names and switch to `envVarsSecret`. #[derive(Debug, Serialize, Clone)] pub struct ZitadelScore { - /// Host used for external access (ingress) + /// External domain (e.g. `"auth.example.com"`). pub host: String, + pub zitadel_version: String, } -impl Score for ZitadelScore { +impl Score for ZitadelScore { fn name(&self) -> String { "ZitadelScore".to_string() } #[doc(hidden)] fn create_interpret(&self) -> Box> { - // TODO exec pod commands to initialize secret store if not already done + Box::new(ZitadelInterpret { + host: self.host.clone(), + zitadel_version: self.zitadel_version.clone(), + }) + } +} + +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone)] +struct ZitadelInterpret { + host: String, + zitadel_version: String, +} + +#[async_trait] +impl Interpret for ZitadelInterpret { + async fn execute( + &self, + inventory: &Inventory, + topology: &T, + ) -> Result { + info!( + "[Zitadel] Starting full deployment — namespace: '{NAMESPACE}', host: '{}'", + self.host + ); + + info!("Creating namespace {NAMESPACE} if it does not exist"); + K8sResourceScore::single( + Namespace { + metadata: ObjectMeta { + name: Some(NAMESPACE.to_string()), + ..Default::default() + }, + ..Default::default() + }, + None, + ) + .interpret(inventory, topology) + .await?; + + // --- Step 1: PostgreSQL ------------------------------------------- + + let pg_config = PostgreSQLConfig { + cluster_name: PG_CLUSTER_NAME.to_string(), + instances: 2, + storage_size: StorageSize::gi(10), + role: PostgreSQLClusterRole::Primary, + namespace: NAMESPACE.to_string(), + }; + + debug!( + "[Zitadel] Deploying PostgreSQL cluster '{}' — instances: {}, storage: 10Gi, namespace: '{}'", + pg_config.cluster_name, pg_config.instances, pg_config.namespace + ); + + topology.deploy(&pg_config).await.map_err(|e| { + let msg = format!( + "[Zitadel] PostgreSQL deployment failed for '{}': {e}", + pg_config.cluster_name + ); + error!("{msg}"); + InterpretError::new(msg) + })?; + + info!( + "[Zitadel] PostgreSQL cluster '{}' deployed", + pg_config.cluster_name + ); + + // --- Step 2: Resolve internal DB endpoint ------------------------- + + debug!( + "[Zitadel] Resolving internal endpoint for cluster '{}'", + pg_config.cluster_name + ); + + let endpoint = topology.get_endpoint(&pg_config).await.map_err(|e| { + let msg = format!( + "[Zitadel] Failed to resolve endpoint for cluster '{}': {e}", + pg_config.cluster_name + ); + error!("{msg}"); + InterpretError::new(msg) + })?; + + info!( + "[Zitadel] DB endpoint resolved — host: '{}', port: {}", + endpoint.host, endpoint.port + ); + + // The CNPG-managed superuser secret contains 'password', 'username', + // 'host', 'port', 'dbname', 'uri'. We reference 'password' directly + // via env.valueFrom.secretKeyRef because CNPG's key names do not + // match ZITADEL's required env var names. + let pg_user_secret = format!("{PG_CLUSTER_NAME}-app"); + let pg_superuser_secret = format!("{PG_CLUSTER_NAME}-superuser"); + let db_host = &endpoint.host; + let db_port = endpoint.port; let host = &self.host; - let values_yaml = Some(format!(r#""#)); + debug!("[Zitadel] DB credentials source — secret: '{pg_user_secret}', key: 'password'"); + debug!( + "[Zitadel] DB credentials source — superuser secret: '{pg_superuser_secret}', key: 'password'" + ); - todo!("This is not complete yet"); + // Zitadel requires one symbol, one number and more. So let's force it. + fn generate_secure_password(length: usize) -> String { + const ALPHA_UPPER: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const ALPHA_LOWER: &[u8] = b"abcdefghijklmnopqrstuvwxyz"; + const DIGITS: &[u8] = b"0123456789"; + const SYMBOLS: &[u8] = b"!@#$%^&*()_+-=[]{}|;:',.<>?/"; - HelmChartScore { - namespace: Some(NonBlankString::from_str("zitadel").unwrap()), + let mut rng = rand::rng(); + let uniform_alpha_upper = rand::distr::Uniform::new(0, ALPHA_UPPER.len()) + .expect("Failed to create distribution"); + let uniform_alpha_lower = rand::distr::Uniform::new(0, ALPHA_LOWER.len()) + .expect("Failed to create distribution"); + let uniform_digits = + rand::distr::Uniform::new(0, DIGITS.len()).expect("Failed to create distribution"); + let uniform_symbols = + rand::distr::Uniform::new(0, SYMBOLS.len()).expect("Failed to create distribution"); + + let mut chars: Vec = Vec::with_capacity(length); + + // Ensure at least one of each: upper, lower, digit, symbol + chars.push(ALPHA_UPPER[uniform_alpha_upper.sample(&mut rng)] as char); + chars.push(ALPHA_LOWER[uniform_alpha_lower.sample(&mut rng)] as char); + chars.push(DIGITS[uniform_digits.sample(&mut rng)] as char); + chars.push(SYMBOLS[uniform_symbols.sample(&mut rng)] as char); + + // Fill remaining with random from all categories + let all_chars: Vec = [ALPHA_UPPER, ALPHA_LOWER, DIGITS, SYMBOLS].concat(); + + let uniform_all = rand::distr::Uniform::new(0, all_chars.len()) + .expect("Failed to create distribution"); + + for _ in 0..(length - 4) { + chars.push(all_chars[uniform_all.sample(&mut rng)] as char); + } + + // Shuffle + let mut shuffled = chars; + shuffled.shuffle(&mut rng); + + return shuffled.iter().collect(); + } + + let admin_password = generate_secure_password(16); + + // --- Step 3: Create masterkey secret ------------------------------------ + + debug!( + "[Zitadel] Creating masterkey secret '{}' in namespace '{}'", + MASTERKEY_SECRET_NAME, NAMESPACE + ); + + // Masterkey for symmetric encryption — must be exactly 32 ASCII bytes (alphanumeric only). + let masterkey = rng() + .sample_iter(&rand::distr::Alphanumeric) + .take(32) + .map(char::from) + .collect::(); + + debug!( + "[Zitadel] Created masterkey secret '{}' in namespace '{}'", + MASTERKEY_SECRET_NAME, NAMESPACE + ); + + let mut masterkey_data: BTreeMap = BTreeMap::new(); + masterkey_data.insert("masterkey".to_string(), ByteString(masterkey.into())); + + let masterkey_secret = Secret { + metadata: ObjectMeta { + name: Some(MASTERKEY_SECRET_NAME.to_string()), + namespace: Some(NAMESPACE.to_string()), + ..ObjectMeta::default() + }, + data: Some(masterkey_data), + ..Secret::default() + }; + + match topology + .k8s_client() + .await + .map_err(|e| InterpretError::new(format!("Failed to get k8s client : {e}")))? + .create(&masterkey_secret, Some(NAMESPACE)) + .await + { + Ok(_) => { + info!( + "[Zitadel] Masterkey secret '{}' created", + MASTERKEY_SECRET_NAME + ); + } + Err(KubeError::Api(ErrorResponse { code: 409, .. })) => { + info!( + "[Zitadel] Masterkey secret '{}' already exists, leaving it untouched", + MASTERKEY_SECRET_NAME + ); + } + Err(other) => { + let msg = format!( + "[Zitadel] Failed to create masterkey secret '{}': {other}", + MASTERKEY_SECRET_NAME + ); + error!("{msg}"); + return Err(InterpretError::new(msg)); + } + }; + + debug!( + "[Zitadel] Masterkey secret '{}' created successfully", + MASTERKEY_SECRET_NAME + ); + + // --- Step 4: Build Helm values ------------------------------------ + + warn!( + "[Zitadel] Applying TLS-enabled ingress defaults for OKD/OpenShift. \ + cert-manager annotations are included as optional hints and are \ + ignored on clusters without cert-manager." + ); + + let values_yaml = format!( + r#"image: + tag: {zitadel_version} +zitadel: + masterkeySecretName: "{MASTERKEY_SECRET_NAME}" + configmapConfig: + ExternalDomain: "{host}" + ExternalSecure: true + FirstInstance: + Org: + Human: + UserName: "admin" + Password: "{admin_password}" + FirstName: "Zitadel" + LastName: "Admin" + Email: "admin@zitadel.example.com" + PasswordChangeRequired: true + TLS: + Enabled: false + Database: + Postgres: + Host: "{db_host}" + Port: {db_port} + Database: zitadel + MaxOpenConns: 20 + MaxIdleConns: 10 + User: + Username: postgres + SSL: + Mode: require + Admin: + Username: postgres + SSL: + Mode: require +# Directly import credentials from the postgres secret +# TODO : use a less privileged postgres user +env: + - name: ZITADEL_DATABASE_POSTGRES_USER_USERNAME + valueFrom: + secretKeyRef: + name: "{pg_superuser_secret}" + key: user + - name: ZITADEL_DATABASE_POSTGRES_USER_PASSWORD + valueFrom: + secretKeyRef: + name: "{pg_superuser_secret}" + key: password + - name: ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME + valueFrom: + secretKeyRef: + name: "{pg_superuser_secret}" + key: user + - name: ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: "{pg_superuser_secret}" + key: password +# Security context for OpenShift restricted PSA compliance +podSecurityContext: + runAsNonRoot: true + runAsUser: null + fsGroup: null + seccompProfile: + type: RuntimeDefault +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + runAsUser: null + fsGroup: null + seccompProfile: + type: RuntimeDefault +# Init job security context (runs before main deployment) +initJob: + podSecurityContext: + runAsNonRoot: true + runAsUser: null + fsGroup: null + seccompProfile: + type: RuntimeDefault + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + runAsUser: null + fsGroup: null + seccompProfile: + type: RuntimeDefault +# Setup job security context +setupJob: + podSecurityContext: + runAsNonRoot: true + runAsUser: null + fsGroup: null + seccompProfile: + type: RuntimeDefault + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + runAsUser: null + fsGroup: null + seccompProfile: + type: RuntimeDefault +ingress: + enabled: true + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + route.openshift.io/termination: edge + hosts: + - host: "{host}" + paths: + - path: / + pathType: Prefix + tls: + - hosts: + - "{host}" + secretName: "{host}-tls" + +login: + enabled: true + podSecurityContext: + runAsNonRoot: true + runAsUser: null + fsGroup: null + seccompProfile: + type: RuntimeDefault + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + runAsUser: null + fsGroup: null + seccompProfile: + type: RuntimeDefault + ingress: + enabled: true + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + route.openshift.io/termination: edge + hosts: + - host: "{host}" + paths: + - path: /ui/v2/login + pathType: Prefix + tls: + - hosts: + - "{host}" + secretName: "{host}-tls""#, + zitadel_version = self.zitadel_version + ); + + trace!("[Zitadel] Helm values YAML:\n{values_yaml}"); + + // --- Step 5: Deploy Helm chart ------------------------------------ + + info!( + "[Zitadel] Deploying Helm chart 'zitadel/zitadel' as release 'zitadel' in namespace '{NAMESPACE}'" + ); + + let result = HelmChartScore { + namespace: Some(NonBlankString::from_str(NAMESPACE).unwrap()), release_name: NonBlankString::from_str("zitadel").unwrap(), chart_name: NonBlankString::from_str("zitadel/zitadel").unwrap(), chart_version: None, values_overrides: None, - values_yaml, + values_yaml: Some(values_yaml), create_namespace: true, install_only: false, repository: Some(HelmRepository::new( @@ -46,6 +479,40 @@ impl Score for ZitadelScore { true, )), } - .create_interpret() + .interpret(inventory, topology) + .await; + + match &result { + Ok(_) => info!( + "[Zitadel] Helm chart deployed successfully\n\n\ + ===== ZITADEL DEPLOYMENT COMPLETE =====\n\ + Login URL: https://{host}\n\ + Username: admin@zitadel.{host}\n\ + Password: {admin_password}\n\n\ + IMPORTANT: The password is saved in ConfigMap 'zitadel-config-yaml'\n\ + and must be changed on first login. Save the credentials in a\n\ + secure location after changing them.\n\ + =========================================" + ), + Err(e) => error!("[Zitadel] Helm chart deployment failed: {e}"), + } + + result + } + + fn get_name(&self) -> InterpretName { + InterpretName::Custom("Zitadel") + } + + fn get_version(&self) -> Version { + todo!() + } + + fn get_status(&self) -> InterpretStatus { + todo!() + } + + fn get_children(&self) -> Vec { + vec![] } }