diff --git a/Cargo.lock b/Cargo.lock index ed3a064..49b4bbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,6 +45,16 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -79,32 +89,6 @@ dependencies = [ "syn", ] -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "atomic" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" - -[[package]] -name = "atomic" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" -dependencies = [ - "bytemuck", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -140,7 +124,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -149,12 +133,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "binascii" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" - [[package]] name = "bitflags" version = "2.9.4" @@ -176,12 +154,6 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" -[[package]] -name = "bytemuck" -version = "1.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" - [[package]] name = "bytes" version = "1.10.1" @@ -214,6 +186,15 @@ dependencies = [ "serde", ] +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -223,17 +204,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "cookie" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - [[package]] name = "core-foundation" version = "0.10.1" @@ -275,15 +245,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "deranged" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" -dependencies = [ - "powerfmt", -] - [[package]] name = "derive_more" version = "2.0.1" @@ -304,39 +265,6 @@ dependencies = [ "syn", ] -[[package]] -name = "devise" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1d90b0c4c777a2cad215e3c7be59ac7c15adf45cf76317009b7d096d46f651d" -dependencies = [ - "devise_codegen", - "devise_core", -] - -[[package]] -name = "devise_codegen" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71b28680d8be17a570a2334922518be6adc3f58ecc880cbb404eaeb8624fd867" -dependencies = [ - "devise_core", - "quote", -] - -[[package]] -name = "devise_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7" -dependencies = [ - "bitflags", - "proc-macro2", - "proc-macro2-diagnostics", - "quote", - "syn", -] - [[package]] name = "digest" version = "0.10.7" @@ -365,15 +293,6 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - [[package]] name = "enum-ordinalize" version = "4.3.0" @@ -400,16 +319,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.0", -] - [[package]] name = "event-listener" version = "5.4.1" @@ -437,20 +346,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -[[package]] -name = "figment" -version = "0.10.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" -dependencies = [ - "atomic 0.6.1", - "pear", - "serde", - "toml", - "uncased", - "version_check", -] - [[package]] name = "find-msvc-tools" version = "0.1.2" @@ -555,19 +450,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generator" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" -dependencies = [ - "cc", - "libc", - "log", - "rustversion", - "windows", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -607,12 +489,6 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - [[package]] name = "gloo-timers" version = "0.3.0" @@ -625,6 +501,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -642,12 +537,6 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - [[package]] name = "home" version = "0.5.11" @@ -668,17 +557,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.3.1" @@ -690,17 +568,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -708,7 +575,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.3.1", + "http", ] [[package]] @@ -719,8 +586,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] @@ -736,29 +603,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - [[package]] name = "hyper" version = "1.7.0" @@ -769,9 +613,11 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "http 1.3.1", - "http-body 1.0.1", + "h2", + "http", + "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -786,8 +632,8 @@ 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", + "hyper", "hyper-util", "log", "rustls", @@ -804,7 +650,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper 1.7.0", + "hyper", "hyper-util", "pin-project-lite", "tokio", @@ -821,12 +667,12 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "hyper 1.7.0", + "http", + "http-body", + "hyper", "libc", "pin-project-lite", - "socket2 0.6.0", + "socket2", "tokio", "tower-service", "tracing", @@ -840,15 +686,29 @@ checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", "hashbrown 0.16.0", - "serde", - "serde_core", ] [[package]] -name = "inlinable_string" -version = "0.1.15" +name = "inotify" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags", + "futures-core", + "inotify-sys", + "libc", + "tokio", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] [[package]] name = "io-uring" @@ -861,17 +721,6 @@ dependencies = [ "libc", ] -[[package]] -name = "is-terminal" -version = "0.4.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "itoa" version = "1.0.15" @@ -939,10 +788,16 @@ dependencies = [ name = "k8s-reboot-coordinator" version = "0.1.0" dependencies = [ + "futures", + "hostname", + "inotify", "k8s-openapi", "kube", "kube-runtime", - "rocket", + "libc", + "mockito", + "serde_json", + "tokio", "tracing", "tracing-subscriber", ] @@ -971,10 +826,10 @@ dependencies = [ "either", "futures", "home", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", - "hyper 1.7.0", + "hyper", "hyper-rustls", "hyper-timeout", "hyper-util", @@ -1004,7 +859,7 @@ dependencies = [ "chrono", "derive_more", "form_urlencoded", - "http 1.3.1", + "http", "json-patch", "k8s-openapi", "serde", @@ -1052,12 +907,6 @@ version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - [[package]] name = "lock_api" version = "0.4.13" @@ -1074,21 +923,6 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" -[[package]] -name = "loom" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "serde", - "serde_json", - "tracing", - "tracing-subscriber", -] - [[package]] name = "matchers" version = "0.2.0" @@ -1131,22 +965,27 @@ dependencies = [ ] [[package]] -name = "multer" -version = "3.1.0" +name = "mockito" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +checksum = "7760e0e418d9b7e5777c0374009ca4c93861b9066f18cb334a20ce50ab63aa48" dependencies = [ + "assert-json-diff", "bytes", - "encoding_rs", + "colored", "futures-util", - "http 1.3.1", - "httparse", - "memchr", - "mime", - "spin", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand", + "regex", + "serde_json", + "serde_urlencoded", + "similar", "tokio", - "tokio-util", - "version_check", ] [[package]] @@ -1158,12 +997,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - [[package]] name = "num-traits" version = "0.2.19" @@ -1173,16 +1006,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "object" version = "0.36.7" @@ -1239,30 +1062,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", -] - -[[package]] -name = "pear" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" -dependencies = [ - "inlinable_string", - "pear_codegen", - "yansi", -] - -[[package]] -name = "pear_codegen" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" -dependencies = [ - "proc-macro2", - "proc-macro2-diagnostics", - "quote", - "syn", + "windows-targets", ] [[package]] @@ -1357,12 +1157,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1381,19 +1175,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "proc-macro2-diagnostics" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "version_check", - "yansi", -] - [[package]] name = "quote" version = "1.0.40" @@ -1411,20 +1192,19 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" -version = "0.8.5" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" -version = "0.3.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core", @@ -1432,11 +1212,11 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.4" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.3.3", ] [[package]] @@ -1448,26 +1228,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "ref-cast" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "regex" version = "1.11.2" @@ -1511,106 +1271,12 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rocket" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a516907296a31df7dc04310e7043b61d71954d703b603cc6867a026d7e72d73f" -dependencies = [ - "async-stream", - "async-trait", - "atomic 0.5.3", - "binascii", - "bytes", - "either", - "figment", - "futures", - "indexmap", - "log", - "memchr", - "multer", - "num_cpus", - "parking_lot", - "pin-project-lite", - "rand", - "ref-cast", - "rocket_codegen", - "rocket_http", - "serde", - "state", - "tempfile", - "time", - "tokio", - "tokio-stream", - "tokio-util", - "ubyte", - "version_check", - "yansi", -] - -[[package]] -name = "rocket_codegen" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46" -dependencies = [ - "devise", - "glob", - "indexmap", - "proc-macro2", - "quote", - "rocket_http", - "syn", - "unicode-xid", - "version_check", -] - -[[package]] -name = "rocket_http" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e274915a20ee3065f611c044bd63c40757396b6dbc057d6046aec27f14f882b9" -dependencies = [ - "cookie", - "either", - "futures", - "http 0.2.12", - "hyper 0.14.32", - "indexmap", - "log", - "memchr", - "pear", - "percent-encoding", - "pin-project-lite", - "ref-cast", - "serde", - "smallvec", - "stable-pattern", - "state", - "time", - "tokio", - "uncased", -] - [[package]] name = "rustc-demangle" version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" -[[package]] -name = "rustix" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.0", -] - [[package]] name = "rustls" version = "0.23.32" @@ -1679,12 +1345,6 @@ dependencies = [ "windows-sys 0.61.0", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" @@ -1777,11 +1437,14 @@ dependencies = [ ] [[package]] -name = "serde_spanned" -version = "0.6.9" +name = "serde_urlencoded" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ + "form_urlencoded", + "itoa", + "ryu", "serde", ] @@ -1833,6 +1496,12 @@ dependencies = [ "libc", ] +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "slab" version = "0.4.11" @@ -1845,16 +1514,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.0" @@ -1865,30 +1524,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "stable-pattern" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4564168c00635f88eaed410d5efa8131afa8d8699a612c80c455a0ba05c21045" -dependencies = [ - "memchr", -] - -[[package]] -name = "state" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8" -dependencies = [ - "loom", -] - [[package]] name = "subtle" version = "2.6.1" @@ -1912,19 +1547,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -[[package]] -name = "tempfile" -version = "3.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" -dependencies = [ - "fastrand", - "getrandom 0.3.3", - "once_cell", - "rustix", - "windows-sys 0.61.0", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -1974,37 +1596,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "time" -version = "0.3.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - [[package]] name = "tokio" version = "1.47.1" @@ -2016,10 +1607,11 @@ dependencies = [ "io-uring", "libc", "mio", + "parking_lot", "pin-project-lite", "signal-hook-registry", "slab", - "socket2 0.6.0", + "socket2", "tokio-macros", "windows-sys 0.59.0", ] @@ -2045,17 +1637,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-stream" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-util" version = "0.7.16" @@ -2070,47 +1651,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - [[package]] name = "tower" version = "0.5.2" @@ -2137,8 +1677,8 @@ dependencies = [ "base64", "bitflags", "bytes", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "mime", "pin-project-lite", "tower-layer", @@ -2232,43 +1772,18 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" -[[package]] -name = "ubyte" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f720def6ce1ee2fc44d40ac9ed6d3a59c361c80a75a7aa8e75bb9baed31cf2ea" -dependencies = [ - "serde", -] - [[package]] name = "ucd-trie" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" -[[package]] -name = "uncased" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" -dependencies = [ - "serde", - "version_check", -] - [[package]] name = "unicode-ident" version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -2385,15 +1900,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-link" version = "0.1.3" @@ -2412,7 +1918,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -2421,7 +1927,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -2433,67 +1939,34 @@ dependencies = [ "windows-link 0.2.0", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2506,78 +1979,36 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" -dependencies = [ - "memchr", -] - [[package]] name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" -dependencies = [ - "is-terminal", -] - [[package]] name = "zerocopy" version = "0.8.27" diff --git a/Cargo.toml b/Cargo.toml index 86e55bc..2c4031c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,12 +4,18 @@ version = "0.1.0" edition = "2024" [dependencies] +futures = { version = "0.3.31", default-features = false, features = ["std"] } +hostname = "0.4.1" +inotify = "0.11.0" k8s-openapi = { version = "0.26.0", features = ["earliest"] } kube = "2.0.1" -rocket = { version = "0.5.1", default-features = false } +tokio = { version = "1.47.1", default-features = false, features = ["macros", "rt", "signal"] } tracing = "0.1.41" tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } [dev-dependencies] kube = { version = "2.0.1", features = ["runtime"] } kube-runtime = "2.0.1" +libc = "0.2.176" +mockito = "1.7.0" +serde_json = "1.0.145" diff --git a/Containerfile b/Containerfile index cca74a8..bb77b71 100644 --- a/Containerfile +++ b/Containerfile @@ -11,10 +11,12 @@ RUN cargo build --release --locked RUN strip target/release/k8s-reboot-coordinator -FROM scratch +FROM docker.io/library/busybox COPY --from=build /src/target/release/k8s-reboot-coordinator / -ENV ROCKET_CLI_COLORS=false +ENV REBOOT_SENTINEL=/host/run/reboot-needed ENTRYPOINT ["/k8s-reboot-coordinator"] + +CMD ["nsenter", "-t", "1", "-m", "-n", "/bin/systemctl", "reboot", "--when", "+5 min"] diff --git a/kubernetes/deployment.yaml b/kubernetes/daemonset.yaml similarity index 61% rename from kubernetes/deployment.yaml rename to kubernetes/daemonset.yaml index 0c0c03e..8e06d30 100644 --- a/kubernetes/deployment.yaml +++ b/kubernetes/daemonset.yaml @@ -1,5 +1,5 @@ apiVersion: apps/v1 -kind: Deployment +kind: DaemonSet metadata: name: k8s-reboot-coordinator labels: @@ -27,24 +27,32 @@ spec: env: - name: RUST_LOG value: info - - name: ROCKET_ADDRESS - value: 0.0.0.0 - startupProbe: - httpGet: - path: /healthz - port: http - periodSeconds: 1 - failureThreshold: 30 - readinessProbe: - httpGet: - path: /healthz - port: http - periodSeconds: 600 - failureThreshold: 3 + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName securityContext: + capabilities: + add: + - CAP_DAC_READ_SEARCH + - CAP_SYS_CHROOT + - CAP_SYS_ADMIN + drop: + - ALL + privileged: true readOnlyRootFilesystem: true + volumeMounts: + - mountPath: /host + name: host + readOnly: true + hostPID: true securityContext: - runAsUser: 15473 - runAsGroup: 15473 - runAsNonRoot: true + runAsUser: 0 + runAsGroup: 0 + runAsNonRoot: false serviceAccountName: k8s-reboot-coordinator + volumes: + - name: host + hostPath: + path: / + type: Directory diff --git a/kubernetes/kustomization.yaml b/kubernetes/kustomization.yaml index 26c8872..53b584c 100644 --- a/kubernetes/kustomization.yaml +++ b/kubernetes/kustomization.yaml @@ -3,4 +3,4 @@ kind: Kustomization resources: - rbac.yaml -- deployment.yaml +- daemonset.yaml diff --git a/src/backoff.rs b/src/backoff.rs new file mode 100644 index 0000000..67567e5 --- /dev/null +++ b/src/backoff.rs @@ -0,0 +1,52 @@ +use std::time::Duration; + +pub struct Backoff { + duration: Duration, + max: Duration, +} + +impl Backoff { + pub fn new(duration: Duration, max: Duration) -> Self { + Self { duration, max } + } + + pub fn next(&mut self) -> Duration { + let duration = self.duration; + self.duration = std::cmp::min(self.duration * 2, self.max); + duration + } +} + +impl Default for Backoff { + fn default() -> Self { + Self::new(Duration::from_millis(250), Duration::from_secs(300)) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_first() { + let mut backoff = Backoff::default(); + assert_eq!(backoff.next(), Duration::from_millis(250)); + } + + #[test] + fn test_second() { + let mut backoff = Backoff::default(); + backoff.next(); + assert_eq!(backoff.next(), Duration::from_millis(500)); + } + + #[test] + fn test_max() { + let mut backoff = + Backoff::new(Duration::from_millis(250), Duration::from_secs(1)); + backoff.next(); + backoff.next(); + backoff.next(); + assert_eq!(backoff.next(), Duration::from_secs(1)); + } +} diff --git a/src/context.rs b/src/context.rs new file mode 100644 index 0000000..5ba0372 --- /dev/null +++ b/src/context.rs @@ -0,0 +1,84 @@ +use std::ffi::OsString; +use std::path::{PathBuf, Path}; + +use tracing::error; + +#[derive(Debug)] +pub struct Context { + reboot_cmd: Vec, + lock_group: String, + node_name: String, + sentinel_path: PathBuf, +} + +impl Context { + fn default_reboot_command() -> Vec { + let cmd = ["systemctl", "reboot"]; + cmd.iter().map(OsString::from).collect() + } + + fn default_lock_group() -> String { + "default".into() + } +} + +impl Context { + pub fn from_env() -> Self { + let reboot_cmd: Vec<_> = std::env::args_os().skip(1).collect(); + Self { + reboot_cmd: if reboot_cmd.is_empty() { + Self::default_reboot_command() + } else { + reboot_cmd + }, + lock_group: get_env("REBOOT_LOCK_GROUP", Self::default_lock_group), + node_name: get_env("NODE_NAME", || { + hostname::get() + .inspect_err(|e| error!("Error getting hostname: {e}")) + .ok() + .and_then(|s| { + s.into_string() + .inspect_err(|_| error!("Invalid hostname: not a valid Unicode value")) + .ok() + }) + .unwrap_or_else(|| "localhost".into()) + }), + sentinel_path: PathBuf::from(get_env("REBOOT_SENTINEL", || { + "/run/reboot-needed".into() + })), + } + } + + pub fn lock_group(&self) -> &str { + self.lock_group.as_ref() + } + + pub fn node_name(&self) -> &str { + self.node_name.as_ref() + } + + pub fn reboot_cmd(&self) -> &[OsString] { + self.reboot_cmd.as_ref() + } + + pub fn sentinel_path(&self) -> &Path { + self.sentinel_path.as_ref() + } +} + +fn get_env(name: &str, default: D) -> String +where + D: FnOnce() -> String, +{ + use std::env::VarError; + match std::env::var(name) { + Ok(v) => v, + Err(VarError::NotPresent) => default(), + Err(VarError::NotUnicode(_)) => { + error!( + "Invalid Unicode value for environment variable {name}, using default" + ); + default() + }, + } +} diff --git a/src/drain.rs b/src/drain.rs index a0402c6..12dd553 100644 --- a/src/drain.rs +++ b/src/drain.rs @@ -1,11 +1,10 @@ use std::collections::{HashMap, HashSet}; +use futures::stream::{BoxStream, StreamExt}; use k8s_openapi::api::core::v1::{Node, Pod}; use kube::Client; use kube::api::{Api, ListParams, WatchEvent, WatchParams}; -use rocket::futures::stream::{BoxStream, StreamExt}; -use rocket::tokio; -use rocket::tokio::sync::mpsc::{self, Receiver}; +use tokio::sync::mpsc::{self, Receiver}; use tracing::{debug, error, info, trace, warn}; async fn wait_drained( diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 73e6ee3..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,28 +0,0 @@ -mod drain; -mod lock; - -use rocket::Request; -use rocket::http::Status; - -#[rocket::catch(default)] -fn not_found(status: Status, _req: &Request) -> String { - match status.reason() { - Some(s) => format!("{s}\n"), - None => format!("{}\n", status.code), - } -} - -#[rocket::get("/healthz")] -fn healthz() -> &'static str { - "UP" -} - -#[rocket::launch] -pub fn rocket() -> _ { - rocket::build() - .mount( - "/", - rocket::routes![healthz, lock::lock_v1, lock::unlock_v1], - ) - .register("/", rocket::catchers![not_found]) -} diff --git a/src/lock.rs b/src/lock.rs index 3fccc43..e58bf32 100644 --- a/src/lock.rs +++ b/src/lock.rs @@ -1,25 +1,16 @@ +use futures::stream::{StreamExt, TryStreamExt}; use k8s_openapi::api::coordination::v1::{Lease, LeaseSpec}; use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; use kube::Client; use kube::api::{Api, Patch, PatchParams, WatchEvent, WatchParams}; -use rocket::form::Form; -use rocket::futures::{StreamExt, TryStreamExt}; -use rocket::http::Status; -use rocket::request::{self, FromRequest, Request}; use tracing::{error, info, trace, warn}; -use crate::drain; +use crate::backoff::Backoff; -#[derive(Debug, rocket::Responder)] +#[derive(Debug)] pub enum LockError { - #[response(status = 500, content_type = "plain")] ServerError(String), - #[response(status = 400, content_type = "plain")] - InvalidHeader(String), - #[response(status = 409, content_type = "plain")] Conflict(String), - #[response(status = 422, content_type = "plain")] - FormError(String), } impl From for LockError { @@ -29,52 +20,20 @@ impl From for LockError { } } -impl From for LockError { - fn from(_h: InvalidHeader) -> Self { - Self::InvalidHeader("Invalid lock header\n".into()) - } -} - -impl From> for LockError { - fn from(errors: rocket::form::Errors<'_>) -> Self { - let mut message = String::from("Error processing request:\n"); - for error in errors { - if let Some(name) = error.name { - message.push_str(&format!("{name}: ")); - } - message.push_str(&error.kind.to_string()); - message.push('\n'); - } - Self::FormError(message) - } -} - -pub struct LockRequestHeader; - -#[derive(Debug)] -pub struct InvalidHeader; - -#[rocket::async_trait] -impl<'r> FromRequest<'r> for LockRequestHeader { - type Error = InvalidHeader; - - async fn from_request( - req: &'r Request<'_>, - ) -> request::Outcome { - match req.headers().get_one("K8s-Reboot-Lock") { - Some("lock") => request::Outcome::Success(Self), - _ => request::Outcome::Error((Status::BadRequest, InvalidHeader)), +impl std::fmt::Display for LockError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::ServerError(e) => write!(f, "{e}"), + Self::Conflict(e) => write!(f, "{e}"), } } } -#[derive(rocket::FromForm)] -pub struct LockRequest { - hostname: String, - #[field(default = String::from("default"))] - group: String, - #[field(default = true)] - wait: bool, +impl std::error::Error for LockError {} + +async fn get_lease(client: Client, name: &str) -> Result { + let leases: Api = Api::default_namespaced(client); + leases.get(name).await } async fn update_lease( @@ -102,94 +61,554 @@ async fn wait_lease(client: Client, name: &str) -> Result<(), kube::Error> { let leases: Api = Api::default_namespaced(client); let params = WatchParams::default().fields(&format!("metadata.name={name}")); - let mut stream = leases.watch(¶ms, "0").await?.boxed(); - while let Some(event) = stream.try_next().await? { - trace!("Watch lease event: {event:?}"); - match event { - WatchEvent::Added(l) | WatchEvent::Modified(l) => match l.spec { - Some(spec) if spec.holder_identity.is_some() => (), - _ => break, - }, - WatchEvent::Bookmark(_) => (), - WatchEvent::Deleted(_) => break, - WatchEvent::Error(e) => return Err(kube::Error::Api(e)), + let mut backoff = Backoff::default(); + loop { + let mut stream = leases.watch(¶ms, "0").await?.boxed(); + while let Some(event) = stream.try_next().await? { + trace!("Watch lease event: {event:?}"); + match event { + WatchEvent::Added(l) | WatchEvent::Modified(l) => { + match l.spec { + Some(spec) if spec.holder_identity.is_some() => (), + _ => return Ok(()), + } + }, + WatchEvent::Bookmark(_) => (), + WatchEvent::Deleted(_) => return Ok(()), + WatchEvent::Error(e) => return Err(kube::Error::Api(e)), + } } + warn!("Lost watch stream"); + tokio::time::sleep(backoff.next()).await; } - Ok(()) } -#[rocket::post("/api/v1/lock", data = "")] -pub async fn lock_v1( - lockheader: Result, - data: rocket::form::Result<'_, Form>, -) -> Result { - lockheader?; - let data = data?; - let client = Client::try_default().await.inspect_err(|e| { - error!("Could not connect to Kubernetes API server: {e}") - })?; - let lock_name = format!("reboot-lock-{}", data.group); - loop { - match update_lease( - client.clone(), - &lock_name, - &data.hostname, - Some(&data.hostname), - ) - .await - { - Ok(_) => break, - Err(kube::Error::Api(e)) => { - if e.code == 409 { - warn!("Lock already held: {}", e.message); - if !data.wait { - return Err(LockError::Conflict(format!( - "Another system is already rebooting: {}\n", - e.message, - ))); - } else { - info!("Waiting for lease {lock_name}"); - if let Err(e) = - wait_lease(client.clone(), &lock_name).await - { - error!("Error while waiting for lease: {e}"); +pub struct Lock { + client: Client, + pub name: String, + pub holder: String, +} + +impl Lock { + pub fn new, H: Into>( + client: Client, + group: G, + hostname: H, + ) -> Self { + Self { + client, + name: format!("reboot-lock-{}", group.as_ref()), + holder: hostname.into(), + } + } + + pub async fn acquire(&self, wait: bool) -> Result<(), LockError> { + loop { + match update_lease( + self.client.clone(), + &self.name, + &self.holder, + Some(&self.holder), + ) + .await + { + Ok(_) => break, + Err(kube::Error::Api(e)) => { + if e.code == 409 { + warn!("Lock already held: {}", e.message); + if !wait { + return Err(LockError::Conflict(format!( + "Another system is already rebooting: {}\n", + e.message, + ))); + } else { + info!("Waiting for lease {}", self.name); + if let Err(e) = + wait_lease(self.client.clone(), &self.name) + .await + { + error!("Error while waiting for lease: {e}"); + } } + } else { + return Err(kube::Error::Api(e).into()); + } + }, + Err(e) => return Err(e.into()), + } + } + Ok(()) + } + + pub async fn is_held(&self) -> Result { + match get_lease(self.client.clone(), &self.name).await { + Ok(l) => { + if let Some(spec) = l.spec { + if let Some(holder) = spec.holder_identity { + Ok(holder == self.holder) + } else { + Ok(false) } } else { - return Err(kube::Error::Api(e).into()); + Ok(false) } }, - Err(e) => return Err(e.into()), + Err(kube::Error::Api(e)) if e.code == 404 => Ok(false), + Err(e) => Err(e)?, } } - if let Err(e) = drain::cordon_node(client.clone(), &data.hostname).await { - error!("Failed to cordon node {}: {e}", data.hostname); - } else if let Err(e) = - drain::drain_node(client.clone(), &data.hostname).await - { - error!("Failed to drain node {}: {e}", data.hostname); + + pub async fn release(&self) -> Result<(), LockError> { + update_lease(self.client.clone(), &self.name, &self.holder, None) + .await?; + Ok(()) } - Ok(format!( - "Acquired reboot lock for group {}, host {}\n", - data.group, data.hostname - )) } -#[rocket::post("/api/v1/unlock", data = "")] -pub async fn unlock_v1( - lockheader: Result, - data: rocket::form::Result<'_, Form>, -) -> Result<(), LockError> { - lockheader?; - let data = data?; - let client = Client::try_default().await.inspect_err(|e| { - error!("Could not connect to Kubernetes API server: {e}") - })?; - let lock_name = format!("reboot-lock-{}", data.group); - update_lease(client.clone(), &lock_name, &data.hostname, None).await?; - if let Err(e) = drain::uncordon_node(client.clone(), &data.hostname).await - { - error!("Failed to uncordon node {}: {e}", data.hostname); +#[cfg(test)] +mod test { + use super::*; + + use mockito::Matcher; + use serde_json::json; + + async fn mock_client() -> (mockito::ServerGuard, Client) { + let server = mockito::Server::new_async().await; + let config = kube::Config::new(server.url().parse().unwrap()); + let client = Client::try_from(config).unwrap(); + (server, client) + } + + fn make_lease(holder: Option<&str>) -> serde_json::Value { + let mut lease = json!({ + "kind": "Lease", + "apiVersion": "coordination.k8s.io/v1", + "metadata": { + "name": "reboot-lock-default", + "namespace": "default", + "uid": "a0eb6ab0-94d3-4973-8091-0b670876a750", + "resourceVersion": "24015", + "creationTimestamp": "2025-09-20T13:18:22Z", + "managedFields": [ + ] + }, + "spec": {} + }); + if let Some(holder) = holder { + lease["spec"]["holderIdentity"] = holder.into(); + let mf = + lease["metadata"]["managedFields"].as_array_mut().unwrap(); + mf.push(json!({ + "manager": holder, + "operation": "Apply", + "apiVersion": "coordination.k8s.io/v1", + "time": "2025-09-20T13:18:22Z", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:spec": { + "f:holderIdentity": {} + } + } + } + )) + } + lease + } + + fn leases_url_path(namespace: &str) -> String { + format!("/apis/coordination.k8s.io/v1/namespaces/{namespace}/leases") + } + + fn lease_url_path(namespace: &str, name: &str) -> String { + format!("{}/{name}", leases_url_path(namespace)) + } + + #[tokio::test] + async fn test_get_lease_notfound() { + let (mut server, client) = mock_client().await; + let mock_lease = server + .mock("GET", &*lease_url_path("default", "reboot-lock-default")) + .with_status(404) + .create(); + let lease = get_lease(client, "reboot-lock-default").await; + mock_lease.assert(); + assert!(matches!(lease, Err(kube::Error::Api(e)) if e.code == 404)); + } + + #[tokio::test] + async fn test_get_lease_success() { + let (mut server, client) = mock_client().await; + let lease_json = make_lease(None); + let mock_lease = server + .mock("GET", &*lease_url_path("default", "reboot-lock-default")) + .with_status(200) + .with_header("Content-Type", "application/json") + .with_body(lease_json.to_string()) + .create(); + let lease = get_lease(client, "reboot-lock-default").await.unwrap(); + mock_lease.assert(); + assert_eq!( + lease.metadata.name.as_deref(), + Some("reboot-lock-default") + ); + } + + #[tokio::test] + async fn test_lock_is_held_notfound() { + let (mut server, client) = mock_client().await; + let mock_lease = server + .mock("GET", &*lease_url_path("default", "reboot-lock-default")) + .with_status(404) + .create(); + let lock = Lock::new(client, "default", "test1.example.org"); + let is_held = lock.is_held().await.unwrap(); + mock_lease.assert(); + assert!(!is_held); + } + + #[tokio::test] + async fn test_lock_is_held_no_holder() { + let (mut server, client) = mock_client().await; + let lease_json = make_lease(None); + let mock_lease = server + .mock("GET", &*lease_url_path("default", "reboot-lock-default")) + .with_status(200) + .with_header("Content-Type", "application/json") + .with_body(lease_json.to_string()) + .create(); + let lock = Lock::new(client, "default", "test1.example.org"); + let is_held = lock.is_held().await.unwrap(); + mock_lease.assert(); + assert!(!is_held); + } + + #[tokio::test] + async fn test_lock_is_held_other_holder() { + let (mut server, client) = mock_client().await; + let lease_json = make_lease(Some("test1.example.org")); + let mock_lease = server + .mock("GET", &*lease_url_path("default", "reboot-lock-default")) + .with_status(200) + .with_header("Content-Type", "application/json") + .with_body(lease_json.to_string()) + .create(); + let lock = Lock::new(client, "default", "test2.example.org"); + let is_held = lock.is_held().await.unwrap(); + mock_lease.assert(); + assert!(!is_held); + } + + #[tokio::test] + async fn test_lock_is_held_true() { + let (mut server, client) = mock_client().await; + let lease_json = make_lease(Some("test1.example.org")); + let mock_lease = server + .mock("GET", &*lease_url_path("default", "reboot-lock-default")) + .with_status(200) + .with_body(lease_json.to_string()) + .create(); + let lock = Lock::new(client, "default", "test1.example.org"); + let is_held = lock.is_held().await.unwrap(); + mock_lease.assert(); + assert!(is_held); + } + + #[tokio::test] + async fn test_lock_acquire_success() { + let (mut server, client) = mock_client().await; + let lease_json = make_lease(Some("test1.example.org")); + let mock_lease = server + .mock("PATCH", &*lease_url_path("default", "reboot-lock-default")) + .match_query(Matcher::UrlEncoded( + "fieldManager".into(), + "test1.example.org".into(), + )) + .match_body( + &*json!({ + "apiVersion": "coordination.k8s.io/v1", + "kind": "Lease", + "metadata": { + "name": "reboot-lock-default" + }, + "spec": { + "holderIdentity": "test1.example.org" + } + }) + .to_string(), + ) + .with_status(200) + .with_body(lease_json.to_string()) + .create(); + let lock = Lock::new(client, "default", "test1.example.org"); + let result = lock.acquire(false).await; + mock_lease.assert(); + assert!(matches!(result, Ok(()))); + } + + #[tokio::test] + async fn test_lock_acquire_conflict() { + let (mut server, client) = mock_client().await; + let mock_lease = server + .mock("PATCH", &*lease_url_path("default", "reboot-lock-default")) + .match_query(Matcher::UrlEncoded( + "fieldManager".into(), + "test1.example.org".into(), + )) + .match_body( + &*json!({ + "apiVersion": "coordination.k8s.io/v1", + "kind": "Lease", + "metadata": { + "name": "reboot-lock-default" + }, + "spec": { + "holderIdentity": "test1.example.org" + } + }) + .to_string(), + ) + .with_status(409) + .create(); + let lock = Lock::new(client, "default", "test1.example.org"); + let result = lock.acquire(false).await; + mock_lease.assert(); + assert!(matches!(result, Err(LockError::Conflict(_)))); + } + + #[tokio::test] + async fn test_lock_acquire_wait_deleted() { + use std::sync::{Arc, Mutex}; + let _tsdg = crate::test::setup(); + let orig_lease = make_lease(Some("test1.example.org")); + let new_lease = make_lease(Some("test8.example.org")); + let (mut server, client) = mock_client().await; + let x1 = Arc::new(Mutex::new(true)); + let x2 = x1.clone(); + let x3 = x1.clone(); + let patch1 = server + .mock("PATCH", &*lease_url_path("default", "reboot-lock-default")) + .match_query(Matcher::UrlEncoded( + "fieldManager".into(), + "test8.example.org".into(), + )) + .match_request(move |_| *x1.lock().unwrap()) + .with_status(409) + .create(); + let wait = server + .mock("GET", &*leases_url_path("default")) + .match_query(Matcher::AllOf(vec![ + Matcher::UrlEncoded("watch".into(), "true".into()), + Matcher::UrlEncoded("timeoutSeconds".into(), "290".into()), + Matcher::UrlEncoded( + "fieldSelector".into(), + "metadata.name=reboot-lock-default".into(), + ), + Matcher::UrlEncoded( + "allowWatchBookmarks".into(), + "true".into(), + ), + Matcher::UrlEncoded("resourceVersion".into(), "0".into()), + ])) + .with_status(200) + .with_chunked_body(move |w| { + w.write_all( + json!({ + "type": "ADDED", + "object": orig_lease + }) + .to_string() + .as_bytes(), + )?; + w.write_all("\n".as_bytes())?; + std::thread::sleep(std::time::Duration::from_millis(250)); + w.write_all( + json!({ + "type": "DELETED", + "object": orig_lease + }) + .to_string() + .as_bytes(), + )?; + w.write_all("\n".as_bytes())?; + *x2.lock().unwrap() = false; + Ok(()) + }) + .create(); + let patch2 = server + .mock("PATCH", &*lease_url_path("default", "reboot-lock-default")) + .match_query(Matcher::UrlEncoded( + "fieldManager".into(), + "test8.example.org".into(), + )) + .match_request(move |_| !*x3.lock().unwrap()) + .with_status(200) + .with_body(new_lease.to_string()) + .create(); + let lock = Lock::new(client, "default", "test8.example.org"); + let result = tokio::time::timeout( + std::time::Duration::from_secs(1), + lock.acquire(true), + ) + .await; + patch1.assert(); + wait.assert(); + patch2.assert(); + match result { + Err(e) => panic!("{e}"), + Ok(Err(e)) => panic!("{e}"), + Ok(_) => (), + }; + } + + #[tokio::test] + async fn test_lock_acquire_wait_released() { + use std::sync::{Arc, Mutex}; + let _tsdg = crate::test::setup(); + let orig_lease = make_lease(Some("test1.example.org")); + let free_lease = make_lease(None); + let new_lease = make_lease(Some("test9.example.org")); + let (mut server, client) = mock_client().await; + let x1 = Arc::new(Mutex::new(true)); + let x2 = x1.clone(); + let x3 = x1.clone(); + let patch1 = server + .mock("PATCH", &*lease_url_path("default", "reboot-lock-default")) + .match_query(Matcher::UrlEncoded( + "fieldManager".into(), + "test9.example.org".into(), + )) + .match_request(move |_| *x1.lock().unwrap()) + .with_status(409) + .create(); + let wait = server + .mock("GET", &*leases_url_path("default")) + .match_query(Matcher::AllOf(vec![ + Matcher::UrlEncoded("watch".into(), "true".into()), + Matcher::UrlEncoded("timeoutSeconds".into(), "290".into()), + Matcher::UrlEncoded( + "fieldSelector".into(), + "metadata.name=reboot-lock-default".into(), + ), + Matcher::UrlEncoded( + "allowWatchBookmarks".into(), + "true".into(), + ), + Matcher::UrlEncoded("resourceVersion".into(), "0".into()), + ])) + .with_status(200) + .with_chunked_body(move |w| { + w.write_all( + json!({ + "type": "ADDED", + "object": orig_lease + }) + .to_string() + .as_bytes(), + )?; + w.write_all("\n".as_bytes())?; + std::thread::sleep(std::time::Duration::from_millis(250)); + w.write_all( + json!({ + "type": "MODIFIED", + "object": free_lease + }) + .to_string() + .as_bytes(), + )?; + w.write_all("\n".as_bytes())?; + *x2.lock().unwrap() = false; + Ok(()) + }) + .create(); + let patch2 = server + .mock("PATCH", &*lease_url_path("default", "reboot-lock-default")) + .match_query(Matcher::UrlEncoded( + "fieldManager".into(), + "test9.example.org".into(), + )) + .match_request(move |_| !*x3.lock().unwrap()) + .with_status(200) + .with_body(new_lease.to_string()) + .create(); + let lock = Lock::new(client, "default", "test9.example.org"); + let result = tokio::time::timeout( + std::time::Duration::from_secs(1), + lock.acquire(true), + ) + .await; + patch1.assert(); + wait.assert(); + patch2.assert(); + match result { + Err(e) => panic!("{e}"), + Ok(Err(e)) => panic!("{e}"), + Ok(_) => (), + }; + } + + #[tokio::test] + async fn test_lock_release_created() { + let _tsdg = crate::test::setup(); + let (mut server, client) = mock_client().await; + let lease = make_lease(None); + let mock = server + .mock("PATCH", &*lease_url_path("default", "reboot-lock-default")) + .match_query(Matcher::UrlEncoded( + "fieldManager".into(), + "test4.example.org".into(), + )) + .with_status(201) + .with_body(lease.to_string()) + .create(); + let lock = Lock::new(client, "default", "test4.example.org"); + let result = lock.release().await; + mock.assert(); + if result.is_err() { + panic!("{result:?}"); + } + } + + #[tokio::test] + async fn test_lock_release_owned() { + let _tsdg = crate::test::setup(); + let (mut server, client) = mock_client().await; + let lease = make_lease(Some("test4.example.org")); + let mock = server + .mock("PATCH", &*lease_url_path("default", "reboot-lock-default")) + .match_query(Matcher::UrlEncoded( + "fieldManager".into(), + "test4.example.org".into(), + )) + .with_status(200) + .with_body(lease.to_string()) + .create(); + let lock = Lock::new(client, "default", "test4.example.org"); + let result = lock.release().await; + mock.assert(); + if result.is_err() { + panic!("{result:?}"); + } + } + + #[tokio::test] + async fn test_lock_release_not_owned() { + let _tsdg = crate::test::setup(); + let (mut server, client) = mock_client().await; + let lease = make_lease(Some("test1.example.org")); + let mock = server + .mock("PATCH", &*lease_url_path("default", "reboot-lock-default")) + .match_query(Matcher::UrlEncoded( + "fieldManager".into(), + "test4.example.org".into(), + )) + .with_status(200) + .with_body(lease.to_string()) + .create(); + let lock = Lock::new(client, "default", "test4.example.org"); + let result = lock.release().await; + mock.assert(); + if result.is_err() { + panic!("{result:?}"); + } } - Ok(()) } diff --git a/src/main.rs b/src/main.rs index fa9f5ee..3c2edfd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,168 @@ -#[rocket::launch] -fn rocket() -> _ { +mod backoff; +mod context; +mod drain; +mod lock; + +#[cfg(test)] +mod test; + +use std::ffi::OsString; +use std::io::ErrorKind; +use std::os::unix::process::CommandExt; +use std::path::Path; +use std::process::Command; +use std::time::Duration; + +use inotify::{EventMask, Inotify, WatchMask}; +use tokio::signal::unix::{SignalKind, signal}; +use tracing::{debug, error, info}; + +use crate::backoff::Backoff; +use crate::context::Context; + +#[tokio::main(flavor = "current_thread")] +async fn main() { tracing_subscriber::fmt() .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) .with_writer(std::io::stderr) .init(); - k8s_reboot_coordinator::rocket() + let client = match kube::Client::try_default().await { + Ok(c) => c, + Err(e) => { + error!("Failed to initialize Kubernetes client: {e}"); + std::process::exit(1); + }, + }; + let task = tokio::spawn(async move { + if let Err(e) = inner_main(client).await { + error!("Unexpected error: {e}"); + } + }); + + if let Err(e) = wait_signalled().await { + error!("Error setting up signal handlers: {e}"); + std::process::exit(1); + } + task.abort(); + if let Err(e) = task.await { + if e.is_panic() { + std::panic::resume_unwind(e.into_panic()); + } + } +} + +async fn inner_main( + client: kube::Client, +) -> Result<(), Box> { + let ctx = Context::from_env(); + let lock = + lock::Lock::new(client.clone(), ctx.lock_group(), ctx.node_name()); + + release_lock(client.clone(), &lock).await; + wait_sentinel(&ctx).await?; + acquire_lock(client.clone(), &lock, &ctx).await; + + info!("Initiating node reboot"); + exec_cmd(ctx.reboot_cmd()); + unreachable!(); +} + +async fn acquire_lock(client: kube::Client, lock: &lock::Lock, ctx: &Context) { + let mut backoff = Backoff::default(); + info!("Waiting to acquire reboot lock"); + while let Err(e) = lock.acquire(true).await { + error!("Failed to acquire reboot lock: {e}"); + tokio::time::sleep(backoff.next()).await; + } + + if let Err(e) = drain::cordon_node(client.clone(), ctx.node_name()).await { + error!("Error cordoning node: {e}"); + } else if let Err(e) = + drain::drain_node(client.clone(), ctx.node_name()).await + { + error!("Error draining node: {e}"); + } +} + +fn exec_cmd(cmd: &[OsString]) { + let program = &cmd[0]; + let args = &cmd[1..]; + let error = Command::new(program).args(args).exec(); + let rc = match error.kind() { + ErrorKind::NotFound => 127, + ErrorKind::PermissionDenied => 126, + _ => 1, + }; + eprintln!("{}: {error}", program.to_string_lossy()); + std::process::exit(rc); +} + +async fn release_lock(client: kube::Client, lock: &lock::Lock) { + let mut backoff = + Backoff::new(Duration::from_millis(50), Duration::from_secs(10)); + loop { + match lock.is_held().await { + Ok(false) => { + debug!("Did not hold reboot lock"); + return; + }, + Ok(true) => break, + Err(e) => { + error!("Failed to check lock status: {e}"); + }, + } + tokio::time::sleep(backoff.next()).await; + } + info!("Releasing reboot lock"); + if let Err(e) = lock.release().await { + error!("Failed to release reboot lock: {e}"); + } else if let Err(e) = drain::uncordon_node(client, &lock.holder).await { + error!("Failed to uncordon node: {e}"); + } +} + +async fn wait_signalled() -> std::io::Result<()> { + let mut sigterm = signal(SignalKind::terminate())?; + let mut sigint = signal(SignalKind::interrupt())?; + tokio::select! { + _ = sigterm.recv() => info!("Received SIGTERM"), + _ = sigint.recv() => info!("Received SIGINT"), + } + Ok(()) +} + +async fn wait_sentinel(context: &Context) -> std::io::Result<()> { + use futures::TryStreamExt; + if context.sentinel_path().metadata().is_ok() { + info!("Sentinel file already exists"); + return Ok(()); + } + let inotify = Inotify::init()?; + let watch_dir = context + .sentinel_path() + .parent() + .unwrap_or_else(|| Path::new("/")); + debug!( + "Watching for sentinel file: {}", + context.sentinel_path().display() + ); + inotify.watches().add(watch_dir, WatchMask::CREATE)?; + let mut buf = [0; 1024]; + let mut stream = inotify.into_event_stream(&mut buf)?; + while let Some(evt) = stream.try_next().await? { + match evt { + inotify::Event { + mask: EventMask::CREATE, + wd: _, + cookie: _, + name, + } if name.as_deref() == context.sentinel_path().file_name() => { + info!("Reboot sentinel appeared"); + break; + }, + _ => (), + } + } + Ok(()) } diff --git a/src/test.rs b/src/test.rs new file mode 100644 index 0000000..3d1fe79 --- /dev/null +++ b/src/test.rs @@ -0,0 +1,9 @@ +pub(crate) fn setup() -> tracing::subscriber::DefaultGuard { + use tracing::Level; + tracing::subscriber::set_default( + tracing_subscriber::fmt() + .with_max_level(Level::TRACE) + .with_test_writer() + .finish(), + ) +} diff --git a/tests/integration/basic.rs b/tests/integration/basic.rs deleted file mode 100644 index 9b2c4ca..0000000 --- a/tests/integration/basic.rs +++ /dev/null @@ -1,21 +0,0 @@ -use rocket::http::Status; - -#[test] -fn test_healthz() { - super::setup(); - - let client = super::client(); - let response = client.get("/healthz").dispatch(); - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.into_string(), Some("UP".into())); -} - -#[test] -fn test_not_found() { - super::setup(); - - let client = super::client(); - let response = client.get("/bogus").dispatch(); - assert_eq!(response.status(), Status::NotFound); - assert_eq!(response.into_string(), Some("Not Found\n".into())); -} diff --git a/tests/integration/lock.rs b/tests/integration/lock.rs deleted file mode 100644 index aa093cc..0000000 --- a/tests/integration/lock.rs +++ /dev/null @@ -1,461 +0,0 @@ -use std::sync::LazyLock; - -use k8s_openapi::api::coordination::v1::Lease; -use k8s_openapi::api::core::v1::{Node, Pod}; -use kube::Client; -use kube::api::{Api, ListParams}; -use rocket::async_test; -use rocket::futures::FutureExt; -use rocket::http::{ContentType, Header, Status}; -use rocket::tokio; -use rocket::tokio::sync::Mutex; - -static LOCK: LazyLock> = LazyLock::new(|| Mutex::new(())); - -async fn delete_lease(name: &str) { - let client = Client::try_default().await.unwrap(); - let leases: Api = Api::default_namespaced(client); - let _ = kube::runtime::wait::delete::delete_and_finalize( - leases, - name, - &Default::default(), - ) - .await; -} - -async fn get_lease(name: &str) -> Result { - let client = Client::try_default().await.unwrap(); - let leases: Api = Api::default_namespaced(client); - leases.get(name).await -} - -async fn get_a_node() -> Result { - let client = Client::try_default().await?; - let nodes: Api = Api::all(client); - Ok(nodes.list(&Default::default()).await?.items.pop().unwrap()) -} - -async fn get_node_by_name(name: &str) -> Result { - let client = Client::try_default().await?; - let nodes: Api = Api::all(client); - nodes.get(name).await -} - -async fn get_pods_on_node(name: &str) -> Result, kube::Error> { - let client = Client::try_default().await?; - let pods: Api = Api::all(client); - Ok(pods - .list(&ListParams::default().fields(&format!("spec.nodeName=={name}"))) - .await? - .items) -} - -#[async_test] -async fn test_lock_v1_success() { - super::setup(); - let _lock = &*LOCK.lock().await; - - delete_lease("reboot-lock-default").await; - let client = super::async_client().await; - let response = client - .post("/api/v1/lock") - .header(Header::new("K8s-Reboot-Lock", "lock")) - .header(ContentType::Form) - .body("hostname=test1.example.org") - .dispatch() - .await; - assert_eq!(response.status(), Status::Ok); - assert_eq!( - response.into_string().await.as_deref(), - Some( - "Acquired reboot lock for group default, host test1.example.org\n" - ) - ); - let lease = get_lease("reboot-lock-default").await.unwrap(); - assert_eq!( - lease.spec.unwrap().holder_identity.as_deref(), - Some("test1.example.org") - ); -} - -#[async_test] -async fn test_lock_v1_custom_group() { - super::setup(); - - delete_lease("reboot-lock-testgroup").await; - let client = super::async_client().await; - let response = client - .post("/api/v1/lock") - .header(Header::new("K8s-Reboot-Lock", "lock")) - .header(ContentType::Form) - .body("hostname=test1.example.org&group=testgroup") - .dispatch() - .await; - assert_eq!(response.status(), Status::Ok); - assert_eq!( - response.into_string().await.as_deref(), - Some( - "Acquired reboot lock for group testgroup, host test1.example.org\n" - ) - ); - let lease = get_lease("reboot-lock-testgroup").await.unwrap(); - assert_eq!( - lease.spec.unwrap().holder_identity.as_deref(), - Some("test1.example.org") - ); -} - -#[async_test] -async fn test_lock_v1_conflict() { - super::setup(); - let _lock = &*LOCK.lock().await; - - delete_lease("reboot-lock-default").await; - let client = super::async_client().await; - let response = client - .post("/api/v1/lock") - .header(Header::new("K8s-Reboot-Lock", "lock")) - .header(ContentType::Form) - .body("hostname=test1.example.org") - .dispatch() - .await; - assert_eq!(response.status(), Status::Ok); - assert_eq!( - response.into_string().await.as_deref(), - Some( - "Acquired reboot lock for group default, host test1.example.org\n" - ) - ); - let response = client - .post("/api/v1/lock") - .header(Header::new("K8s-Reboot-Lock", "lock")) - .header(ContentType::Form) - .body("hostname=test2.example.org&wait=false") - .dispatch() - .await; - assert_eq!(response.status(), Status::Conflict); - let want_msg = concat!( - "Another system is already rebooting:", - " Apply failed with 1 conflict:", - " conflict with \"test1.example.org\":", - " .spec.holderIdentity", - "\n", - ); - assert_eq!(response.into_string().await.as_deref(), Some(want_msg)); - let lease = get_lease("reboot-lock-default").await.unwrap(); - assert_eq!( - lease.spec.unwrap().holder_identity.as_deref(), - Some("test1.example.org") - ); -} - -#[async_test] -async fn test_lock_v1_conflict_wait() { - super::setup(); - let _lock = &*LOCK.lock().await; - - tracing::info!("Deleting existing lease"); - delete_lease("reboot-lock-default").await; - tracing::info!("Creating first lease"); - let client = super::async_client().await; - let response = client - .post("/api/v1/lock") - .header(Header::new("K8s-Reboot-Lock", "lock")) - .header(ContentType::Form) - .body("hostname=test1.example.org") - .dispatch() - .await; - assert_eq!(response.status(), Status::Ok); - assert_eq!( - response.into_string().await.as_deref(), - Some( - "Acquired reboot lock for group default, host test1.example.org\n" - ) - ); - let lease = get_lease("reboot-lock-default").await.unwrap(); - assert_eq!( - lease.spec.unwrap().holder_identity.as_deref(), - Some("test1.example.org") - ); - let timer = std::time::Instant::now(); - let _task = tokio::spawn(async { - tokio::time::sleep(std::time::Duration::from_secs(1)) - .then(|_| async { - tracing::info!("Deleting first lease"); - delete_lease("reboot-lock-default").await - }) - .await - }); - tracing::info!("Creating second lease"); - let response = client - .post("/api/v1/lock") - .header(Header::new("K8s-Reboot-Lock", "lock")) - .header(ContentType::Form) - .body("hostname=test2.example.org") - .dispatch() - .await; - assert_eq!(response.status(), Status::Ok); - assert_eq!( - response.into_string().await.as_deref(), - Some( - "Acquired reboot lock for group default, host test2.example.org\n" - ) - ); - let duration = timer.elapsed().as_millis(); - assert!(duration > 1000 && duration < 2000); - let lease = get_lease("reboot-lock-default").await.unwrap(); - assert_eq!( - lease.spec.unwrap().holder_identity.as_deref(), - Some("test2.example.org") - ); -} - -#[test] -fn test_lock_v1_no_header() { - super::setup(); - - let client = super::client(); - let response = client - .post("/api/v1/lock") - .header(ContentType::Form) - .body("hostname=test1.example.org") - .dispatch(); - assert_eq!(response.status(), Status::BadRequest); - assert_eq!( - response.into_string().as_deref(), - Some("Invalid lock header\n") - ); -} - -#[test] -fn test_lock_v1_no_data() { - super::setup(); - - let client = super::client(); - let response = client - .post("/api/v1/lock") - .header(Header::new("K8s-Reboot-Lock", "lock")) - .header(ContentType::Form) - .body("") - .dispatch(); - assert_eq!(response.status(), Status::UnprocessableEntity); - assert_eq!( - response.into_string().as_deref(), - Some("Error processing request:\nhostname: missing\n") - ); -} - -#[async_test] -async fn test_unlock_v1_success() { - super::setup(); - let _lock = &*LOCK.lock().await; - - delete_lease("reboot-lock-default").await; - let client = super::async_client().await; - let response = client - .post("/api/v1/lock") - .header(Header::new("K8s-Reboot-Lock", "lock")) - .header(ContentType::Form) - .body("hostname=test1.example.org") - .dispatch() - .await; - assert_eq!(response.status(), Status::Ok); - let lease = get_lease("reboot-lock-default").await.unwrap(); - assert_eq!( - lease.spec.unwrap().holder_identity.as_deref(), - Some("test1.example.org") - ); - let response = client - .post("/api/v1/unlock") - .header(Header::new("K8s-Reboot-Lock", "lock")) - .header(ContentType::Form) - .body("hostname=test1.example.org") - .dispatch() - .await; - let status = response.status(); - assert_eq!(response.into_string().await, None); - assert_eq!(status, Status::Ok); - let lease = get_lease("reboot-lock-default").await.unwrap(); - assert_eq!(lease.spec.unwrap().holder_identity, None); -} - -#[async_test] -async fn test_unlock_v1_not_locked() { - super::setup(); - let _lock = &*LOCK.lock().await; - - delete_lease("reboot-lock-default").await; - let client = super::async_client().await; - let response = client - .post("/api/v1/unlock") - .header(Header::new("K8s-Reboot-Lock", "lock")) - .header(ContentType::Form) - .body("hostname=test1.example.org") - .dispatch() - .await; - let status = response.status(); - assert_eq!(response.into_string().await, None); - assert_eq!(status, Status::Ok); - let lease = get_lease("reboot-lock-default").await.unwrap(); - assert_eq!(lease.spec.unwrap().holder_identity.as_deref(), None); -} - -#[async_test] -async fn test_unlock_v1_not_mine() { - super::setup(); - let _lock = &*LOCK.lock().await; - - delete_lease("reboot-lock-default").await; - let client = super::async_client().await; - let response = client - .post("/api/v1/lock") - .header(Header::new("K8s-Reboot-Lock", "lock")) - .header(ContentType::Form) - .body("hostname=test1.example.org") - .dispatch() - .await; - assert_eq!(response.status(), Status::Ok); - let lease = get_lease("reboot-lock-default").await.unwrap(); - assert_eq!( - lease.spec.unwrap().holder_identity.as_deref(), - Some("test1.example.org") - ); - let response = client - .post("/api/v1/unlock") - .header(Header::new("K8s-Reboot-Lock", "lock")) - .header(ContentType::Form) - .body("hostname=test2.example.org") - .dispatch() - .await; - let status = response.status(); - assert_eq!(response.into_string().await, None); - assert_eq!(status, Status::Ok); - let lease = get_lease("reboot-lock-default").await.unwrap(); - assert_eq!( - lease.spec.unwrap().holder_identity.as_deref(), - Some("test1.example.org") - ); -} - -#[test] -fn test_unlock_v1_no_header() { - super::setup(); - - let client = super::client(); - let response = client - .post("/api/v1/unlock") - .header(ContentType::Form) - .body("hostname=test1.example.org") - .dispatch(); - assert_eq!(response.status(), Status::BadRequest); - assert_eq!( - response.into_string().as_deref(), - Some("Invalid lock header\n") - ); -} - -#[test] -fn test_unlock_v1_no_data() { - super::setup(); - - let client = super::client(); - let response = client - .post("/api/v1/unlock") - .header(Header::new("K8s-Reboot-Lock", "lock")) - .header(ContentType::Form) - .body("") - .dispatch(); - assert_eq!(response.status(), Status::UnprocessableEntity); - assert_eq!( - response.into_string().as_deref(), - Some("Error processing request:\nhostname: missing\n") - ); -} - -#[async_test] -async fn test_lock_v1_drain() { - super::setup(); - let _lock = &*LOCK.lock().await; - - delete_lease("reboot-lock-default").await; - let node = get_a_node().await.unwrap(); - let hostname = node.metadata.name.clone().unwrap(); - let client = super::async_client().await; - let response = client - .post("/api/v1/lock") - .header(Header::new("K8s-Reboot-Lock", "lock")) - .header(ContentType::Form) - .body(format!("hostname={hostname}")) - .dispatch() - .await; - let status = response.status(); - assert_eq!( - response.into_string().await, - Some(format!( - "Acquired reboot lock for group default, host {hostname}\n" - )) - ); - assert_eq!(status, Status::Ok); - let lease = get_lease("reboot-lock-default").await.unwrap(); - assert_eq!( - lease.spec.unwrap().holder_identity.as_ref(), - Some(&hostname) - ); - let node = get_node_by_name(&hostname).await.unwrap(); - assert!( - node.spec - .unwrap() - .taints - .unwrap() - .iter() - .any(|t| t.key == "node.kubernetes.io/unschedulable" - && t.effect == "NoSchedule") - ); - let pods = get_pods_on_node(&hostname).await.unwrap(); - assert_eq!( - pods.iter() - .filter(|p| { - !p.metadata - .owner_references - .clone() - .unwrap_or_default() - .iter() - .any(|o| o.kind == "DaemonSet") - }) - .count(), - 0 - ); -} - -#[async_test] -async fn test_unlock_v1_uncordon() { - super::setup(); - let _lock = &*LOCK.lock().await; - - let node = get_a_node().await.unwrap(); - let hostname = node.metadata.name.clone().unwrap(); - let client = super::async_client().await; - let response = client - .post("/api/v1/unlock") - .header(Header::new("K8s-Reboot-Lock", "lock")) - .header(ContentType::Form) - .body(format!("hostname={hostname}")) - .dispatch() - .await; - let status = response.status(); - assert_eq!(response.into_string().await, None,); - assert_eq!(status, Status::Ok); - let lease = get_lease("reboot-lock-default").await.unwrap(); - assert_ne!(lease.spec.unwrap().holder_identity.as_ref(), Some(&hostname)); - let node = get_node_by_name(&hostname).await.unwrap(); - assert!( - !node - .spec - .unwrap() - .taints - .unwrap_or_default() - .iter() - .any(|t| t.key == "node.kubernetes.io/unschedulable" - && t.effect == "NoSchedule") - ); -} diff --git a/tests/integration/main.rs b/tests/integration/main.rs deleted file mode 100644 index e7b05f6..0000000 --- a/tests/integration/main.rs +++ /dev/null @@ -1,36 +0,0 @@ -mod basic; -mod lock; - -use std::sync::LazyLock; - -use rocket::local::asynchronous::Client as AsyncClient; -use rocket::local::blocking::Client; - -static SETUP: LazyLock<()> = LazyLock::new(|| { - unsafe { - std::env::set_var("ROCKET_CLI_COLORS", "false"); - } - tracing_subscriber::fmt() - .with_env_filter(tracing_subscriber::EnvFilter::new(concat!( - env!("CARGO_PKG_NAME"), - "=trace,", - "k8s_reboot_coordinator=trace,", - "info", - ))) - .with_test_writer() - .init(); -}); - -fn setup() { - LazyLock::force(&SETUP); -} - -async fn async_client() -> AsyncClient { - AsyncClient::tracked(k8s_reboot_coordinator::rocket()) - .await - .unwrap() -} - -fn client() -> Client { - Client::tracked(k8s_reboot_coordinator::rocket()).unwrap() -} diff --git a/tests/krc-it-test/main.rs b/tests/krc-it-test/main.rs new file mode 100644 index 0000000..6fdcbc6 --- /dev/null +++ b/tests/krc-it-test/main.rs @@ -0,0 +1,307 @@ +use std::collections::HashMap; +use std::io::ErrorKind; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Output, Stdio}; +use std::sync::LazyLock; + +use k8s_openapi::api::coordination::v1::{Lease, LeaseSpec}; +use k8s_openapi::api::core::v1::{Node, Pod}; +use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; +use kube::Client; +use kube::api::{Api, ListParams, Patch, PatchParams}; +use tokio::sync::Mutex; + +static EXE: &str = env!("CARGO_BIN_EXE_k8s-reboot-coordinator"); +static LOCK: LazyLock> = LazyLock::new(|| Mutex::new(())); + +async fn cordon_node(name: &str) -> Result<(), kube::Error> { + let client = Client::try_default().await?; + let nodes: Api = Api::all(client); + nodes.cordon(name).await?; + Ok(()) +} + +async fn create_lease( + name: &str, + identity: &str, +) -> Result { + let client = Client::try_default().await.unwrap(); + let apply = PatchParams::apply(identity); + let leases: Api = Api::default_namespaced(client); + let lease = Lease { + metadata: ObjectMeta { + name: Some(name.into()), + ..Default::default() + }, + spec: Some(LeaseSpec { + holder_identity: Some(identity.into()), + ..Default::default() + }), + }; + leases.patch(name, &apply, &Patch::Apply(&lease)).await +} + +fn delete_file>(path: P) -> std::io::Result<()> { + match std::fs::remove_file(path) { + Ok(_) => Ok(()), + Err(e) if e.kind() == ErrorKind::NotFound => Ok(()), + Err(e) => Err(e), + } +} + +async fn delete_lease(name: &str) { + let client = Client::try_default().await.unwrap(); + let leases: Api = Api::default_namespaced(client); + let _ = kube::runtime::wait::delete::delete_and_finalize( + leases, + name, + &Default::default(), + ) + .await; +} + +async fn get_a_node() -> Result { + let client = Client::try_default().await?; + let nodes: Api = Api::all(client); + Ok(nodes.list(&Default::default()).await?.items.pop().unwrap()) +} + +async fn get_lease(name: &str) -> Result { + let client = Client::try_default().await.unwrap(); + let leases: Api = Api::default_namespaced(client); + leases.get(name).await +} + +async fn get_node_by_name(name: &str) -> Result { + let client = Client::try_default().await?; + let nodes: Api = Api::all(client); + nodes.get(name).await +} + +async fn get_pods_on_node(name: &str) -> Result, kube::Error> { + let client = Client::try_default().await?; + let pods: Api = Api::all(client); + Ok(pods + .list(&ListParams::default().fields(&format!("spec.nodeName=={name}"))) + .await? + .items) +} + +fn new_sentinel(name: &str) -> PathBuf { + let mut sentinel = std::env::temp_dir(); + sentinel.push(name); + delete_file(&sentinel).unwrap(); + sentinel +} + +fn run_it( + node_name: &str, + sentinel: &Path, + reboot_group: Option<&str>, +) -> std::io::Result { + let mut env: HashMap<_, _> = std::env::vars_os() + .filter(|(k, _)| k == "PATH" || k == "KUBECONFIG" || k == "RUST_LOG") + .collect(); + env.insert("NODE_NAME".into(), node_name.into()); + env.insert("REBOOT_SENTINEL".into(), sentinel.into()); + if let Some(g) = reboot_group { + env.insert("REBOOT_LOCK_GROUP".into(), g.into()); + } + Command::new(EXE) + .args(["echo", "test success"]) + .env_clear() + .envs(&env) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() +} + +fn signal(mut child: Child, signal: i32) -> std::io::Result { + let Ok(pid) = i32::try_from(child.id()) else { + let _ = child.kill(); + let _ = child.wait(); + panic!("Cannot send SIGTERM to child: PID {} too large", child.id()); + }; + unsafe { libc::kill(pid, signal) }; + child.wait_with_output() +} + +fn trigger(mut child: Child, sentinel: &Path) -> std::io::Result { + if let Err(e) = std::fs::File::create(sentinel) { + let _ = child.kill(); + let _ = child.wait(); + panic!("Failed to create sentinel file: {e}"); + } + child.wait_with_output() +} + +async fn uncordon_node(name: &str) -> Result<(), kube::Error> { + let client = Client::try_default().await?; + let nodes: Api = Api::all(client); + nodes.uncordon(name).await?; + Ok(()) +} + + +#[tokio::test] +async fn test_success() { + let _lock = &*LOCK.lock().await; + + delete_lease("reboot-lock-default").await; + let sentinel = new_sentinel("a278f32c-6cf9-4890-b50e-06d3f0c5259c"); + let node_name = "7bc09505-ebd2-432a-ab05-a83d309c501a"; + let child = run_it(node_name, &sentinel, None).unwrap(); + std::thread::sleep(std::time::Duration::from_millis(250)); + let output = trigger(child, &sentinel).unwrap(); + eprint!("{}", str::from_utf8(&output.stderr).unwrap()); + assert_eq!(str::from_utf8(&output.stdout), Ok("test success\n")); + let lease = get_lease("reboot-lock-default").await.unwrap(); + assert_eq!( + lease.spec.unwrap().holder_identity.as_deref(), + Some(node_name) + ) +} + +#[tokio::test] +async fn test_alt_group() { + let _lock = &*LOCK.lock().await; + + delete_lease("reboot-lock-group2").await; + let sentinel = new_sentinel("fc7cb011-f9ed-46ff-a310-76278bbd21de"); + let node_name = "71124880-f4d4-44ff-a18d-f301837e0907"; + let child = run_it(node_name, &sentinel, Some("group2")).unwrap(); + std::thread::sleep(std::time::Duration::from_millis(250)); + let output = trigger(child, &sentinel).unwrap(); + eprint!("{}", str::from_utf8(&output.stderr).unwrap()); + assert_eq!(str::from_utf8(&output.stdout), Ok("test success\n")); + let lease = get_lease("reboot-lock-group2").await.unwrap(); + assert_eq!( + lease.spec.unwrap().holder_identity.as_deref(), + Some(node_name) + ) +} + +#[tokio::test] +async fn test_terminated() { + let _lock = &*LOCK.lock().await; + + delete_lease("reboot-lock-default").await; + let sentinel = new_sentinel("36cdc91b-a2d7-4845-a726-721ecc9787a7"); + let node_name = "a763c92e-5db7-4806-8dd0-e1a72d42b022"; + let child = run_it(node_name, &sentinel, None).unwrap(); + std::thread::sleep(std::time::Duration::from_millis(250)); + let output = signal(child, libc::SIGTERM).unwrap(); + eprint!("{}", str::from_utf8(&output.stderr).unwrap()); + assert_eq!(str::from_utf8(&output.stdout), Ok("")); + assert!(matches!( + get_lease("reboot-lock-default").await, + Err(kube::Error::Api(e)) if e.code == 404 + )); +} + +#[tokio::test] +async fn test_node_not_locked() { + let _lock = &*LOCK.lock().await; + + delete_lease("reboot-lock-default").await; + let sentinel = new_sentinel("115041f9-abef-496c-abc2-df7c9fbcc046"); + let node = get_a_node().await.unwrap(); + let node_name = node.metadata.name.unwrap(); + let child = run_it(&node_name, &sentinel, None).unwrap(); + std::thread::sleep(std::time::Duration::from_millis(250)); + let output = trigger(child, &sentinel).unwrap(); + eprint!("{}", str::from_utf8(&output.stderr).unwrap()); + assert_eq!(str::from_utf8(&output.stdout), Ok("test success\n")); + let lease = get_lease("reboot-lock-default").await.unwrap(); + assert_eq!( + lease.spec.unwrap().holder_identity.as_ref(), + Some(&node_name) + ); + let node = get_node_by_name(&node_name).await.unwrap(); + let pods = get_pods_on_node(&node_name).await.unwrap(); + uncordon_node(&node_name).await.unwrap(); + assert!( + node.spec + .unwrap() + .taints + .unwrap() + .iter() + .any(|t| t.key == "node.kubernetes.io/unschedulable" + && t.effect == "NoSchedule") + ); + assert_eq!( + pods.iter() + .filter(|p| { + !p.metadata + .owner_references + .clone() + .unwrap_or_default() + .iter() + .any(|o| o.kind == "DaemonSet") + }) + .count(), + 0 + ); +} + +#[tokio::test] +async fn test_node_locked() { + let _lock = &*LOCK.lock().await; + + delete_lease("reboot-lock-default").await; + let sentinel = new_sentinel("e5a2fc90-4418-4556-a039-32a2425b1bd9"); + let node = get_a_node().await.unwrap(); + let node_name = node.metadata.name.unwrap(); + create_lease("reboot-lock-default", &node_name) + .await + .unwrap(); + cordon_node(&node_name).await.unwrap(); + let child = run_it(&node_name, &sentinel, None).unwrap(); + std::thread::sleep(std::time::Duration::from_millis(500)); + let lease = get_lease("reboot-lock-default").await.unwrap(); + assert_eq!(lease.spec.unwrap().holder_identity.as_ref(), None,); + let node = get_node_by_name(&node_name).await.unwrap(); + assert!( + !node.spec + .unwrap() + .taints + .unwrap_or_default() + .iter() + .any(|t| t.key == "node.kubernetes.io/unschedulable" + && t.effect == "NoSchedule") + ); + let output = trigger(child, &sentinel).unwrap(); + eprint!("{}", str::from_utf8(&output.stderr).unwrap()); + assert_eq!(str::from_utf8(&output.stdout), Ok("test success\n")); + let lease = get_lease("reboot-lock-default").await.unwrap(); + assert_eq!( + lease.spec.unwrap().holder_identity.as_ref(), + Some(&node_name) + ); + let node = get_node_by_name(&node_name).await.unwrap(); + let pods = get_pods_on_node(&node_name).await.unwrap(); + uncordon_node(&node_name).await.unwrap(); + assert!( + node.spec + .unwrap() + .taints + .unwrap() + .iter() + .any(|t| t.key == "node.kubernetes.io/unschedulable" + && t.effect == "NoSchedule") + ); + assert_eq!( + pods.iter() + .filter(|p| { + !p.metadata + .owner_references + .clone() + .unwrap_or_default() + .iter() + .any(|o| o.kind == "DaemonSet") + }) + .count(), + 0 + ); +}