From 1f9bccf0358be760327967a37545f7722b4bd2b3 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Mon, 24 Aug 2026 12:45:28 +0200 Subject: [PATCH 1/4] added missing ownerships and early-exit from reconcile --- Cargo.lock | 115 +++++++-------- Cargo.nix | 133 +++++++++--------- Cargo.toml | 1 + .../templates/clusterrole-operator.yaml | 3 +- rust/operator-binary/Cargo.toml | 1 + rust/operator-binary/src/controller.rs | 4 + rust/operator-binary/src/main.rs | 15 +- 7 files changed, 145 insertions(+), 127 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 48918fa3..d8600515 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,7 +169,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -316,9 +316,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" -version = "1.4.3" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", "jobserver", @@ -385,7 +385,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -491,9 +491,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" dependencies = [ "cfg-if", ] @@ -547,12 +547,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88490bf1b990d87eaaa7ac8aa887f629a08e7359765b4911faf63c3763347d23" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" dependencies = [ - "darling_core 0.24.0", - "darling_macro 0.24.0", + "darling_core 0.24.1", + "darling_macro 0.24.1", ] [[package]] @@ -570,15 +570,15 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084e274f91c482280130e1e34e0b8d6e66776a060d7b6de7b84289ca778868c4" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" dependencies = [ "ident_case", "proc-macro2", "quote", "strsim", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -594,13 +594,13 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f5792fa0d41cd2325ce0ffa64f0a340eaebd4971a3a0c5e1ffd2cc488a355e" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ - "darling_core 0.24.0", + "darling_core 0.24.1", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -716,7 +716,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -780,14 +780,14 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] name = "either" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "elliptic-curve" @@ -853,7 +853,7 @@ checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -1007,7 +1007,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -1146,9 +1146,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27" +checksum = "839c0e8a181239723652be9062bb56ca5bf5f64011f73b623f6f4fc59086a228" dependencies = [ "atomic-waker", "bytes", @@ -1426,9 +1426,9 @@ checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", @@ -1709,7 +1709,7 @@ name = "k8s-version" version = "0.1.3" source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.116.0#7b9f9ac9a76fa425ab27f2821377ef86571ca121" dependencies = [ - "darling 0.24.0", + "darling 0.24.1", "regex", "snafu 0.9.2", ] @@ -1855,9 +1855,9 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libgit2-sys" -version = "0.18.7+1.9.6" +version = "0.18.8+1.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23c7391e4b9f4ffab1a624223cc1d7385ff9a678f490768add717de7ea2f4d89" +checksum = "7f7c568b25d7489bc3fb2988ed69ab111d2944d2f5fec3d5c987fe545ea97b50" dependencies = [ "cc", "libc", @@ -1900,9 +1900,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "matchers" @@ -2506,22 +2506,22 @@ dependencies = [ [[package]] name = "ref-cast" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -2738,9 +2738,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.14" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", @@ -2800,7 +2800,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -2917,7 +2917,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -2928,7 +2928,7 @@ checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -3178,6 +3178,7 @@ dependencies = [ "clap", "const_format", "futures", + "http", "indoc", "rstest", "serde", @@ -3239,10 +3240,10 @@ name = "stackable-operator-derive" version = "0.3.1" source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.116.0#7b9f9ac9a76fa425ab27f2821377ef86571ca121" dependencies = [ - "darling 0.24.0", + "darling 0.24.1", "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -3307,7 +3308,7 @@ source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-ope dependencies = [ "convert_case", "convert_case_extras", - "darling 0.24.0", + "darling 0.24.1", "indoc", "itertools 0.15.0", "k8s-openapi", @@ -3315,7 +3316,7 @@ dependencies = [ "kube", "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -3413,9 +3414,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" dependencies = [ "proc-macro2", "quote", @@ -3479,7 +3480,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -3577,7 +3578,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -3949,9 +3950,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.24.1" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" +checksum = "f053576934f05a761a402421fbbe3d425d9366f75f978806a037b3ca481abecc" dependencies = [ "js-sys", "wasm-bindgen", @@ -4381,9 +4382,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.7" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b5c6b5976d66c1d703c4fd17d3f5e43c8cedaacf604961b171adc7130896d8" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke", "zerofrom", @@ -4392,13 +4393,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f212a141d820099d57ffafb9569be9617a6f27d3dc881fbee8fb56642f917a9" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] diff --git a/Cargo.nix b/Cargo.nix index a78713ff..57aa469c 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -508,7 +508,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.3"; + packageId = "syn 3.0.4"; usesDefaultFeatures = false; features = [ "clone-impls" "full" "parsing" "printing" "proc-macro" "visit-mut" ]; } @@ -989,9 +989,9 @@ rec { }; "cc" = rec { crateName = "cc"; - version = "1.4.3"; + version = "1.4.4"; edition = "2021"; - sha256 = "0v9b5arr047vbihfbh3fmbd3aj9vf1i7dbdgfpvlwzynpjvr35ah"; + sha256 = "0wq26vvhzv5ci9gx3cfiw320skvaysf9i701wp668lks6ps39m8a"; dependencies = [ { name = "find-msvc-tools"; @@ -1220,7 +1220,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.3"; + packageId = "syn 3.0.4"; features = [ "full" ]; } ]; @@ -1528,9 +1528,9 @@ rec { }; "crc32fast" = rec { crateName = "crc32fast"; - version = "1.5.0"; + version = "1.5.1"; edition = "2021"; - sha256 = "04d51liy8rbssra92p0qnwjw8i9rm9c4m3bwy19wjamz1k4w30cl"; + sha256 = "0l75bfakpwr86wz45gm38lylrpgbssr529fmm6m445qy2rqwi644"; authors = [ "Sam Rijs " "Alex Crichton " @@ -1681,22 +1681,22 @@ rec { }; resolvedDefaultFeatures = [ "default" "suggestions" ]; }; - "darling 0.24.0" = rec { + "darling 0.24.1" = rec { crateName = "darling"; - version = "0.24.0"; + version = "0.24.1"; edition = "2021"; - sha256 = "08vx6iikfg7nz88ljnvnb5rqx819ys3si2mclym7xn4hp7qhnjc8"; + sha256 = "1v625grpyqddgaslgc0kzha41vzqy91yyg80ra9vjc363f8ga5zd"; authors = [ "Ted Driggs " ]; dependencies = [ { name = "darling_core"; - packageId = "darling_core 0.24.0"; + packageId = "darling_core 0.24.1"; } { name = "darling_macro"; - packageId = "darling_macro 0.24.0"; + packageId = "darling_macro 0.24.1"; } ]; features = { @@ -1746,11 +1746,11 @@ rec { }; resolvedDefaultFeatures = [ "strsim" "suggestions" ]; }; - "darling_core 0.24.0" = rec { + "darling_core 0.24.1" = rec { crateName = "darling_core"; - version = "0.24.0"; + version = "0.24.1"; edition = "2021"; - sha256 = "1i38i1vwm2a2p3knsyqd0rm7frkfil5lxqz1600ji0n4j57jfkh8"; + sha256 = "1zyxlb9ypzb3pzm0l8jy09x2kvd7x7qd50b1z0caxal5fk7y4dv8"; authors = [ "Ted Driggs " ]; @@ -1774,7 +1774,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.3"; + packageId = "syn 3.0.4"; features = [ "full" "extra-traits" ]; } ]; @@ -1810,11 +1810,11 @@ rec { ]; }; - "darling_macro 0.24.0" = rec { + "darling_macro 0.24.1" = rec { crateName = "darling_macro"; - version = "0.24.0"; + version = "0.24.1"; edition = "2021"; - sha256 = "0pimi94crlpzw72s18vi96ysw3il197sdzz0bhrd476ll0ppkxb8"; + sha256 = "1197l1qqsxssys3nnz58x36nv5kzpbjipsxv6dw2yazh7rf17ira"; procMacro = true; authors = [ "Ted Driggs " @@ -1822,7 +1822,7 @@ rec { dependencies = [ { name = "darling_core"; - packageId = "darling_core 0.24.0"; + packageId = "darling_core 0.24.1"; } { name = "quote"; @@ -1830,7 +1830,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.3"; + packageId = "syn 3.0.4"; } ]; @@ -2198,7 +2198,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.3"; + packageId = "syn 3.0.4"; } ]; features = { @@ -2406,13 +2406,13 @@ rec { } { name = "syn"; - packageId = "syn 3.0.3"; + packageId = "syn 3.0.4"; } ]; devDependencies = [ { name = "syn"; - packageId = "syn 3.0.3"; + packageId = "syn 3.0.4"; features = [ "full" ]; } ]; @@ -2424,9 +2424,9 @@ rec { }; "either" = rec { crateName = "either"; - version = "1.17.0"; + version = "1.18.0"; edition = "2021"; - sha256 = "07dagpwcfdzpkb1n7fxkx0q3nv80rnf81v7gwlz9ljx22mn8yply"; + sha256 = "0d7dx31sf8rakcgp63070ngb2vkjynrni866pnx879pawndgnai5"; features = { "default" = [ "std" ]; "serde" = [ "dep:serde" ]; @@ -2626,7 +2626,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.3"; + packageId = "syn 3.0.4"; } ]; features = { @@ -3072,7 +3072,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.3"; + packageId = "syn 3.0.4"; features = [ "full" ]; } ]; @@ -3597,9 +3597,9 @@ rec { }; "h2" = rec { crateName = "h2"; - version = "0.4.16"; + version = "0.4.18"; edition = "2021"; - sha256 = "09syqqhvh36b3rwyn8vjhiz597hfki1hcz3hwagb3cs1ifapmwx9"; + sha256 = "0a52hs8cakvg7xi3pxqi83vganyaasxn545ya8v74f8j3250x743"; authors = [ "Carl Lerche " "Sean McArthur " @@ -4584,9 +4584,9 @@ rec { }; "icu_provider" = rec { crateName = "icu_provider"; - version = "2.3.0"; + version = "2.3.1"; edition = "2024"; - sha256 = "0a343jlrb7jlb20xv1airslzv63559wf38jihrx81bba39kyv9wj"; + sha256 = "0wrydhwprwgyka3r3sw6276syjnlw6fpqr2zsm2srvxv7afvnyyj"; authors = [ "The ICU4X Project Developers" ]; @@ -5422,7 +5422,7 @@ rec { dependencies = [ { name = "darling"; - packageId = "darling 0.24.0"; + packageId = "darling 0.24.1"; optional = true; } { @@ -6085,10 +6085,10 @@ rec { }; "libgit2-sys" = rec { crateName = "libgit2-sys"; - version = "0.18.7+1.9.6"; + version = "0.18.8+1.9.7"; edition = "2021"; links = "git2"; - sha256 = "12ad5zmffzbivn57d47lg2kgjprqsz0kq8i4lsqzlkwz9cg3kir3"; + sha256 = "0l3vm5g59zl7r7aw7zpms922j78imdlyv219zg1rnj6p4n5mcz3z"; libName = "libgit2_sys"; libPath = "lib.rs"; authors = [ @@ -6226,9 +6226,9 @@ rec { }; "log" = rec { crateName = "log"; - version = "0.4.33"; + version = "0.4.34"; edition = "2021"; - sha256 = "1bd9dmk22pxgnf0h0slba6rz99zb0a0b2mdhpk8p92bp26ycbvhc"; + sha256 = "1ihkzn0m33ab79fcl4mkb04n5iwqzbxzyw7l7hazqkffaqzbvy7r"; authors = [ "The Rust Project Developers" ]; @@ -6242,11 +6242,12 @@ rec { "kv_unstable_sval" = [ "kv_sval" "kv_unstable" ]; "serde" = [ "serde_core" ]; "serde_core" = [ "dep:serde_core" ]; + "std" = [ "alloc" ]; "sval" = [ "dep:sval" ]; "sval_ref" = [ "dep:sval_ref" ]; "value-bag" = [ "dep:value-bag" ]; }; - resolvedDefaultFeatures = [ "std" ]; + resolvedDefaultFeatures = [ "alloc" "std" ]; }; "matchers" = rec { crateName = "matchers"; @@ -8115,9 +8116,9 @@ rec { }; "ref-cast" = rec { crateName = "ref-cast"; - version = "1.0.26"; + version = "1.0.27"; edition = "2021"; - sha256 = "0vdra0766jcc2czzqwhql41kkfyajdnai1pbkjxbq8vr7mvqyvi1"; + sha256 = "1hv5sf0j7b65gz2g57c3wp0fzr5r3807dywf6fap455lwjs0yi3y"; libName = "ref_cast"; authors = [ "David Tolnay " @@ -8132,9 +8133,9 @@ rec { }; "ref-cast-impl" = rec { crateName = "ref-cast-impl"; - version = "1.0.26"; + version = "1.0.27"; edition = "2021"; - sha256 = "0g70ff9an5i97cw9kijgzqrqydz7smcfic2zyydddizfbxl874ic"; + sha256 = "0fnzgkvddgl9xs3884x5ypi9rd0dgc1p5vd1k4b74lw49ybdiv4j"; procMacro = true; libName = "ref_cast_impl"; authors = [ @@ -8151,7 +8152,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.3"; + packageId = "syn 3.0.4"; } ]; @@ -9072,9 +9073,9 @@ rec { }; "rustls-webpki" = rec { crateName = "rustls-webpki"; - version = "0.103.14"; + version = "0.103.15"; edition = "2021"; - sha256 = "0njk28gvbqrsfg1b5r35y4f80n37kcjylj72fpc0k0g60n3529q5"; + sha256 = "1hhanq3lz384v4nccacnjfwsyy99n3yc6m6iw8kljz8yicfwzhzk"; libName = "webpki"; dependencies = [ { @@ -9275,13 +9276,13 @@ rec { } { name = "syn"; - packageId = "syn 3.0.3"; + packageId = "syn 3.0.4"; } ]; devDependencies = [ { name = "syn"; - packageId = "syn 3.0.3"; + packageId = "syn 3.0.4"; features = [ "extra-traits" ]; } ]; @@ -9647,7 +9648,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.3"; + packageId = "syn 3.0.4"; usesDefaultFeatures = false; features = [ "clone-impls" "derive" "parsing" "printing" "proc-macro" ]; } @@ -9679,7 +9680,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.3"; + packageId = "syn 3.0.4"; usesDefaultFeatures = false; features = [ "clone-impls" "derive" "parsing" "printing" ]; } @@ -10483,6 +10484,10 @@ rec { } ]; devDependencies = [ + { + name = "http"; + packageId = "http"; + } { name = "rstest"; packageId = "rstest"; @@ -10708,7 +10713,7 @@ rec { dependencies = [ { name = "darling"; - packageId = "darling 0.24.0"; + packageId = "darling 0.24.1"; } { name = "proc-macro2"; @@ -10720,7 +10725,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.3"; + packageId = "syn 3.0.4"; } ]; @@ -10992,7 +10997,7 @@ rec { } { name = "darling"; - packageId = "darling 0.24.0"; + packageId = "darling 0.24.1"; } { name = "indoc"; @@ -11029,7 +11034,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.3"; + packageId = "syn 3.0.4"; } ]; @@ -11321,11 +11326,11 @@ rec { }; resolvedDefaultFeatures = [ "clone-impls" "default" "derive" "extra-traits" "fold" "full" "parsing" "printing" "proc-macro" "visit" "visit-mut" ]; }; - "syn 3.0.3" = rec { + "syn 3.0.4" = rec { crateName = "syn"; - version = "3.0.3"; + version = "3.0.4"; edition = "2021"; - sha256 = "18srnql3cd39j9q6hf1az02p67rlr1rf6njx9zx4vxj9i3jvmsak"; + sha256 = "17v4ac61x0hvj1879ywqzlwhyzg7n9lr9zniwrsif3b1ykfmq9z6"; authors = [ "David Tolnay " ]; @@ -11490,7 +11495,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.3"; + packageId = "syn 3.0.4"; } ]; @@ -11854,7 +11859,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.3"; + packageId = "syn 3.0.4"; features = [ "full" ]; } ]; @@ -13240,9 +13245,9 @@ rec { }; "uuid" = rec { crateName = "uuid"; - version = "1.24.1"; + version = "1.25.0"; edition = "2021"; - sha256 = "1n8b7fg7dbx6ws64387l2i0qq900rw9b7qax63acdh37sczw1vrc"; + sha256 = "1k5y394cmcrpl038i5szyxk96pa27nzgn89480d7cnph6ilmflzh"; authors = [ "Ashley Mannix" "Dylan DPC" @@ -15299,9 +15304,9 @@ rec { }; "zerovec" = rec { crateName = "zerovec"; - version = "0.11.7"; + version = "0.11.8"; edition = "2021"; - sha256 = "1n4n109wgbbin5hljq6gmbnqqg74yp9igzf40gbw2rkdjyswddcl"; + sha256 = "1n3xlvyba8riys9s8awy4xp533phqycr78nbsmvdkh86g3hn815v"; authors = [ "The ICU4X Project Developers" ]; @@ -15345,9 +15350,9 @@ rec { }; "zerovec-derive" = rec { crateName = "zerovec-derive"; - version = "0.11.5"; + version = "0.11.6"; edition = "2021"; - sha256 = "1a8pz516ddcgxvxq3j1xgprac5wnprlrbyzsgzarj0423la2l8cz"; + sha256 = "1ni5j8v99x3fcf3l8kp64b7aq4vf8y22jshfq74xs9mxkp1nzprl"; procMacro = true; libName = "zerovec_derive"; authors = [ @@ -15364,7 +15369,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.3"; + packageId = "syn 3.0.4"; features = [ "extra-traits" ]; } ]; diff --git a/Cargo.toml b/Cargo.toml index 3f0b1394..865339d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ built = { version = "0.8", features = ["chrono", "git2"] } clap = "4.6" const_format = "0.2" futures = "0.3" +http = "1.3" indoc = "2.0" rstest = "0.26" semver = "1.0" diff --git a/deploy/helm/kafka-operator/templates/clusterrole-operator.yaml b/deploy/helm/kafka-operator/templates/clusterrole-operator.yaml index 656fed50..358d54c4 100644 --- a/deploy/helm/kafka-operator/templates/clusterrole-operator.yaml +++ b/deploy/helm/kafka-operator/templates/clusterrole-operator.yaml @@ -63,7 +63,7 @@ rules: - list - patch - watch - # PodDisruptionBudget created per role group. Applied via SSA and tracked for orphan cleanup. + # PodDisruptionBudget created per role group. Applied via SSA and tracked for orphan cleanup, and owned by the controller. - apiGroups: - policy resources: @@ -74,6 +74,7 @@ rules: - get - list - patch + - watch # Required for maintaining the CRDs within the operator (including the conversion webhook info). # Also for the startup condition check before the controller can run. - apiGroups: diff --git a/rust/operator-binary/Cargo.toml b/rust/operator-binary/Cargo.toml index b9d9d40f..cf88992b 100644 --- a/rust/operator-binary/Cargo.toml +++ b/rust/operator-binary/Cargo.toml @@ -24,6 +24,7 @@ tokio.workspace = true tracing.workspace = true [dev-dependencies] +http.workspace = true serde_yaml.workspace = true rstest.workspace = true diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 15b53fa6..29ffaceb 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -448,6 +448,10 @@ pub async fn reconcile_kafka( ) -> Result { tracing::info!("Starting reconcile"); + if kafka.meta().deletion_timestamp.is_some() { + return Ok(Action::await_change()); + } + let kafka = kafka .0 .as_ref() diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index 29cc7efa..16ec4f0d 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -16,6 +16,7 @@ use stackable_operator::{ k8s_openapi::api::{ apps::v1::StatefulSet, core::v1::{ConfigMap, Service, ServiceAccount}, + policy::v1::PodDisruptionBudget, rbac::v1::RoleBinding, }, kube::{ @@ -136,19 +137,23 @@ async fn main() -> anyhow::Result<()> { let config_map_store = kafka_controller.store(); let kafka_controller = kafka_controller .owns( - watch_namespace.get_api::(&client), + watch_namespace.get_api::(&client), watcher::Config::default(), ) .owns( - watch_namespace.get_api::(&client), + watch_namespace.get_api::(&client), watcher::Config::default(), ) .owns( - watch_namespace.get_api::(&client), + watch_namespace.get_api::(&client), watcher::Config::default(), ) .owns( - watch_namespace.get_api::(&client), + watch_namespace.get_api::(&client), + watcher::Config::default(), + ) + .owns( + watch_namespace.get_api::(&client), watcher::Config::default(), ) .owns( @@ -156,7 +161,7 @@ async fn main() -> anyhow::Result<()> { watcher::Config::default(), ) .owns( - watch_namespace.get_api::(&client), + watch_namespace.get_api::(&client), watcher::Config::default(), ) .watches( From 0d868ddd6edab9ae315d47dca04610f60499e987 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Mon, 24 Aug 2026 13:29:33 +0200 Subject: [PATCH 2/4] added kuttl test steps and changelog --- CHANGELOG.md | 3 + rust/operator-binary/src/controller.rs | 124 +++++++++++++++++- .../kuttl/cluster-operation/60-assert.yaml | 94 +++++++++++++ .../60-delete-owned-resources.yaml | 53 ++++++++ 4 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 tests/templates/kuttl/cluster-operation/60-assert.yaml create mode 100644 tests/templates/kuttl/cluster-operation/60-delete-owned-resources.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index d4d2282a..b4598838 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ All notable changes to this project will be documented in this file. - Fix a longstanding problem of including empty `categories`, `shortNames` and `additionalPrinterColumns` in the CRDs, which could cause problems with GitOps tools (e.g. ArgoCD) reporting a diff in the custom resources. See [our internal issue](https://github.com/stackabletech/hdfs-operator/issues/626) and [the fix](https://github.com/kube-rs/kube/pull/2042) for details ([#998]). +- The operator now watches all resources that it creates and early-exits the reconcile action when the + cluster is marked for deletion ([#1014]). [#985]: https://github.com/stackabletech/kafka-operator/pull/985 [#990]: https://github.com/stackabletech/kafka-operator/pull/990 @@ -38,6 +40,7 @@ All notable changes to this project will be documented in this file. [#998]: https://github.com/stackabletech/kafka-operator/pull/998 [#1000]: https://github.com/stackabletech/kafka-operator/pull/1000 [#1011]: https://github.com/stackabletech/kafka-operator/pull/1011 +[#1014]: https://github.com/stackabletech/kafka-operator/pull/1014 ## [26.7.0] - 2026-07-21 diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 29ffaceb..7e787cd2 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -627,13 +627,24 @@ pub(crate) mod test_support { #[cfg(test)] mod tests { - use std::collections::BTreeSet; + use std::{collections::BTreeSet, str::FromStr, sync::Arc}; + + use stackable_operator::{ + cli::OperatorEnvironmentOptions, + client::Client, + commons::networking::DomainName, + kube::{Client as KubeClient, Config, core::DeserializeGuard, runtime::controller::Action}, + utils::cluster_info::KubernetesClusterInfo, + }; use super::{ CONTROLLER_NAME, OPERATOR_NAME, PRODUCT_NAME, PodDescriptorsError, test_support::{minimal_kafka, validated_cluster}, }; - use crate::crd::role::KafkaRole; + use crate::{ + controller::{Ctx, Error, reconcile_kafka}, + crd::{role::KafkaRole, v1alpha1}, + }; #[test] fn test_constants() { @@ -720,4 +731,113 @@ mod tests { let node_ids: BTreeSet = descriptors.iter().map(|d| d.node_id).collect(); assert_eq!(node_ids.len(), 3, "node ids must be unique: {node_ids:?}"); } + + /// A [`Ctx`] whose client points at a closed port. Any API call made through it fails the + /// reconciliation, so an `Ok` result proves the reconciler returned before touching the + /// Kubernetes API. + fn unreachable_ctx() -> Arc { + let config = Config::new( + "http://127.0.0.1:1" + .parse::() + .expect("valid static URI"), + ); + let kube_client = KubeClient::try_from(config).expect("client from static config"); + + Arc::new(Ctx { + client: Client::new( + kube_client.clone(), + None, + "default".to_owned(), + KubernetesClusterInfo { + cluster_domain: DomainName::from_str("cluster.local") + .expect("valid cluster domain"), + }, + ), + operator_environment: OperatorEnvironmentOptions { + operator_namespace: "stackable-operators".to_owned(), + operator_service_name: "kafka-operator".to_owned(), + image_repository: "oci.stackable.tech/sdp".to_owned(), + }, + }) + } + + fn reconcile(kafka: DeserializeGuard) -> Result { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("current-thread tokio runtime") + .block_on(async { reconcile_kafka(Arc::new(kafka), unreachable_ctx()).await }) + } + + #[test] + fn reconcile_exits_early_for_deleted_cluster() { + let kafka = serde_yaml::from_str( + r#" +apiVersion: kafka.stackable.tech/v1alpha1 +kind: KafkaCluster +metadata: + name: kafka + namespace: default + deletionTimestamp: "2026-08-14T12:00:00Z" +spec: + image: + productVersion: 3.2.2 +"#, + ) + .expect("valid cluster YAML"); + + let action = reconcile(kafka).expect("a deleted cluster reconciles without any API call"); + + assert_eq!(action, Action::await_change()); + } + + #[test] + fn reconcile_exits_early_for_deleted_cluster_with_invalid_spec() { + let kafka = serde_yaml::from_str( + r#" +apiVersion: kafka.stackable.tech/v1alpha1 +kind: KafkaCluster +metadata: + name: kafka + namespace: default + deletionTimestamp: "2026-08-14T12:00:00Z" +spec: {} +"#, + ) + .expect("YAML parses; the invalid spec is captured inside the DeserializeGuard"); + + let action = + reconcile(kafka).expect("a deleted cluster reconciles even when its spec is invalid"); + + assert_eq!(action, Action::await_change()); + } + + #[test] + fn reconcile_proceeds_for_live_cluster() { + let kafka = serde_yaml::from_str( + r#" +apiVersion: kafka.stackable.tech/v1alpha1 +kind: KafkaCluster +metadata: + name: kafka + namespace: default +spec: + image: + productVersion: 4.1.0 + clusterConfig: {} + brokers: + roleGroups: + default: + replicas: 1 +"#, + ) + .expect("valid cluster YAML"); + + let result = reconcile(kafka); + + assert!( + matches!(result, Err(Error::Dereference { .. })), + "a live cluster must reach the API but when dereferencing against the unreachable test server: {result:?}" + ); + } } diff --git a/tests/templates/kuttl/cluster-operation/60-assert.yaml b/tests/templates/kuttl/cluster-operation/60-assert.yaml new file mode 100644 index 00000000..79971fbe --- /dev/null +++ b/tests/templates/kuttl/cluster-operation/60-assert.yaml @@ -0,0 +1,94 @@ +--- +# The recreated StatefulSet must bring the cluster back to ready, and the recreated +# objects must carry an owner reference back to the KafkaCluster so that garbage +# collection still works for them. +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +metadata: + name: recreate-owned-resources +timeout: 600 +commands: + - script: kubectl -n $NAMESPACE wait --for=condition=available kafkaclusters.kafka.stackable.tech/test-kafka --timeout 601s +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: test-kafka-broker-default + ownerReferences: + - apiVersion: kafka.stackable.tech/v1alpha1 + controller: true + kind: KafkaCluster + name: test-kafka +status: + readyReplicas: 1 + replicas: 1 +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: test-kafka-serviceaccount + ownerReferences: + - apiVersion: kafka.stackable.tech/v1alpha1 + controller: true + kind: KafkaCluster + name: test-kafka +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: test-kafka-rolebinding + ownerReferences: + - apiVersion: kafka.stackable.tech/v1alpha1 + controller: true + kind: KafkaCluster + name: test-kafka +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: test-kafka-broker + ownerReferences: + - apiVersion: kafka.stackable.tech/v1alpha1 + controller: true + kind: KafkaCluster + name: test-kafka +--- +apiVersion: listeners.stackable.tech/v1alpha1 +kind: Listener +metadata: + name: test-kafka-broker-default-bootstrap + ownerReferences: + - apiVersion: kafka.stackable.tech/v1alpha1 + controller: true + kind: KafkaCluster + name: test-kafka +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-kafka + ownerReferences: + - apiVersion: kafka.stackable.tech/v1alpha1 + controller: true + kind: KafkaCluster + name: test-kafka +--- +apiVersion: v1 +kind: Service +metadata: + name: test-kafka-broker-default-headless + ownerReferences: + - apiVersion: kafka.stackable.tech/v1alpha1 + controller: true + kind: KafkaCluster + name: test-kafka +--- +apiVersion: v1 +kind: Service +metadata: + name: test-kafka-broker-default-metrics + ownerReferences: + - apiVersion: kafka.stackable.tech/v1alpha1 + controller: true + kind: KafkaCluster + name: test-kafka diff --git a/tests/templates/kuttl/cluster-operation/60-delete-owned-resources.yaml b/tests/templates/kuttl/cluster-operation/60-delete-owned-resources.yaml new file mode 100644 index 00000000..8e0230d1 --- /dev/null +++ b/tests/templates/kuttl/cluster-operation/60-delete-owned-resources.yaml @@ -0,0 +1,53 @@ +# Every resource the operator applies carries an ownerReference and a `.owns()` watch +# (main.rs); deleting it must trigger a reconcile of the KafkaCluster, which re-applies +# it. This step checks that chain — the ClusterRole's `watch` verbs plus the `.owns()` +# routing — and lives here because this test already cycles the pods. The `.watches()` +# registration (referenced-but-unowned ConfigMaps) can't be tested by deletion: the +# operator never recreates what it didn't apply. +# +# Recreation is proven by UID change: mere existence could pass without any deletion. +# TestStep commands run exactly once (no kuttl retry loop), so the polling stays quiet. +# +# The ConfigMap deleted here is the discovery one, named after the cluster. +# Deliberately skipped: the role-group ConfigMap (test-kafka-broker-default) — +# `.owns(ConfigMap)` is already exercised via the discovery ConfigMap, and deleting a +# ConfigMap mounted into running pods would disturb them. The bootstrap Listener is +# applied by this operator and therefore included, but its backing bootstrap Service +# and the per-broker pod Listeners are created by the listener-operator, so they are +# not part of this check. +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +metadata: + name: delete-owned-resources +timeout: 300 +commands: + - script: | + set -eu + + delete_and_await_recreation() { + kind=$1 + name=$2 + old_uid=$(kubectl get -n "$NAMESPACE" "$kind" "$name" -o jsonpath='{.metadata.uid}') + kubectl delete -n "$NAMESPACE" "$kind" "$name" --wait=false + # Recreation is a single reconcile away, so this normally succeeds on the + # first iteration; 30s is a generous upper bound well below the step timeout. + for _ in $(seq 1 30); do + new_uid=$(kubectl get -n "$NAMESPACE" "$kind" "$name" -o jsonpath='{.metadata.uid}' 2>/dev/null || true) + if [ -n "$new_uid" ] && [ "$new_uid" != "$old_uid" ]; then + return 0 + fi + sleep 1 + done + echo "$kind/$name was not recreated (old uid: $old_uid, current: '${new_uid:-}')" >&2 + return 1 + } + + delete_and_await_recreation serviceaccount test-kafka-serviceaccount + delete_and_await_recreation rolebinding test-kafka-rolebinding + delete_and_await_recreation poddisruptionbudget test-kafka-broker + delete_and_await_recreation listener test-kafka-broker-default-bootstrap + delete_and_await_recreation configmap test-kafka + delete_and_await_recreation service test-kafka-broker-default-headless + delete_and_await_recreation service test-kafka-broker-default-metrics + delete_and_await_recreation statefulset test-kafka-broker-default From 4a3ad0e6c16a0a72fdc45203ce52439e58aa11f9 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Mon, 24 Aug 2026 17:15:18 +0200 Subject: [PATCH 3/4] improve tests, consolidate rbac sections --- Cargo.lock | 1 - Cargo.nix | 4 - Cargo.toml | 1 - rust/operator-binary/Cargo.toml | 1 - rust/operator-binary/src/controller.rs | 127 +++++------------- .../60-delete-owned-resources.yaml | 66 +++++---- 6 files changed, 71 insertions(+), 129 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d8600515..a72bea67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3178,7 +3178,6 @@ dependencies = [ "clap", "const_format", "futures", - "http", "indoc", "rstest", "serde", diff --git a/Cargo.nix b/Cargo.nix index 57aa469c..ccaae9b2 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -10484,10 +10484,6 @@ rec { } ]; devDependencies = [ - { - name = "http"; - packageId = "http"; - } { name = "rstest"; packageId = "rstest"; diff --git a/Cargo.toml b/Cargo.toml index 865339d6..3f0b1394 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,6 @@ built = { version = "0.8", features = ["chrono", "git2"] } clap = "4.6" const_format = "0.2" futures = "0.3" -http = "1.3" indoc = "2.0" rstest = "0.26" semver = "1.0" diff --git a/rust/operator-binary/Cargo.toml b/rust/operator-binary/Cargo.toml index cf88992b..b9d9d40f 100644 --- a/rust/operator-binary/Cargo.toml +++ b/rust/operator-binary/Cargo.toml @@ -24,7 +24,6 @@ tokio.workspace = true tracing.workspace = true [dev-dependencies] -http.workspace = true serde_yaml.workspace = true rstest.workspace = true diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 7e787cd2..e077db3c 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -633,7 +633,7 @@ mod tests { cli::OperatorEnvironmentOptions, client::Client, commons::networking::DomainName, - kube::{Client as KubeClient, Config, core::DeserializeGuard, runtime::controller::Action}, + kube::{Client as KubeClient, Config, runtime::controller::Action}, utils::cluster_info::KubernetesClusterInfo, }; @@ -642,8 +642,8 @@ mod tests { test_support::{minimal_kafka, validated_cluster}, }; use crate::{ - controller::{Ctx, Error, reconcile_kafka}, - crd::{role::KafkaRole, v1alpha1}, + controller::{Ctx, reconcile_kafka}, + crd::role::KafkaRole, }; #[test] @@ -732,71 +732,15 @@ mod tests { assert_eq!(node_ids.len(), 3, "node ids must be unique: {node_ids:?}"); } - /// A [`Ctx`] whose client points at a closed port. Any API call made through it fails the - /// reconciliation, so an `Ok` result proves the reconciler returned before touching the - /// Kubernetes API. - fn unreachable_ctx() -> Arc { - let config = Config::new( - "http://127.0.0.1:1" - .parse::() - .expect("valid static URI"), - ); - let kube_client = KubeClient::try_from(config).expect("client from static config"); - - Arc::new(Ctx { - client: Client::new( - kube_client.clone(), - None, - "default".to_owned(), - KubernetesClusterInfo { - cluster_domain: DomainName::from_str("cluster.local") - .expect("valid cluster domain"), - }, - ), - operator_environment: OperatorEnvironmentOptions { - operator_namespace: "stackable-operators".to_owned(), - operator_service_name: "kafka-operator".to_owned(), - image_repository: "oci.stackable.tech/sdp".to_owned(), - }, - }) - } - - fn reconcile(kafka: DeserializeGuard) -> Result { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("current-thread tokio runtime") - .block_on(async { reconcile_kafka(Arc::new(kafka), unreachable_ctx()).await }) - } - + /// The client points at a closed port, so any API call would fail the reconciliation: an `Ok` + /// proves that a cluster being deleted returns before the reconciler touches the Kubernetes + /// API, and because the spec is invalid, before the `DeserializeGuard` is unwrapped. #[test] fn reconcile_exits_early_for_deleted_cluster() { let kafka = serde_yaml::from_str( r#" apiVersion: kafka.stackable.tech/v1alpha1 kind: KafkaCluster -metadata: - name: kafka - namespace: default - deletionTimestamp: "2026-08-14T12:00:00Z" -spec: - image: - productVersion: 3.2.2 -"#, - ) - .expect("valid cluster YAML"); - - let action = reconcile(kafka).expect("a deleted cluster reconciles without any API call"); - - assert_eq!(action, Action::await_change()); - } - - #[test] - fn reconcile_exits_early_for_deleted_cluster_with_invalid_spec() { - let kafka = serde_yaml::from_str( - r#" -apiVersion: kafka.stackable.tech/v1alpha1 -kind: KafkaCluster metadata: name: kafka namespace: default @@ -806,38 +750,35 @@ spec: {} ) .expect("YAML parses; the invalid spec is captured inside the DeserializeGuard"); - let action = - reconcile(kafka).expect("a deleted cluster reconciles even when its spec is invalid"); + let action = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("current-thread tokio runtime") + .block_on(async { + let ctx = Arc::new(Ctx { + client: Client::new( + KubeClient::try_from(Config::new( + "http://127.0.0.1:1".parse().expect("valid static URI"), + )) + .expect("client from static config"), + None, + "default".to_owned(), + KubernetesClusterInfo { + cluster_domain: DomainName::from_str("cluster.local") + .expect("valid cluster domain"), + }, + ), + operator_environment: OperatorEnvironmentOptions { + operator_namespace: "stackable-operators".to_owned(), + operator_service_name: "kafka-operator".to_owned(), + image_repository: "oci.stackable.tech/sdp".to_owned(), + }, + }); + + reconcile_kafka(Arc::new(kafka), ctx).await + }) + .expect("a deleted cluster reconciles without any API call"); assert_eq!(action, Action::await_change()); } - - #[test] - fn reconcile_proceeds_for_live_cluster() { - let kafka = serde_yaml::from_str( - r#" -apiVersion: kafka.stackable.tech/v1alpha1 -kind: KafkaCluster -metadata: - name: kafka - namespace: default -spec: - image: - productVersion: 4.1.0 - clusterConfig: {} - brokers: - roleGroups: - default: - replicas: 1 -"#, - ) - .expect("valid cluster YAML"); - - let result = reconcile(kafka); - - assert!( - matches!(result, Err(Error::Dereference { .. })), - "a live cluster must reach the API but when dereferencing against the unreachable test server: {result:?}" - ); - } } diff --git a/tests/templates/kuttl/cluster-operation/60-delete-owned-resources.yaml b/tests/templates/kuttl/cluster-operation/60-delete-owned-resources.yaml index 8e0230d1..8c7669f5 100644 --- a/tests/templates/kuttl/cluster-operation/60-delete-owned-resources.yaml +++ b/tests/templates/kuttl/cluster-operation/60-delete-owned-resources.yaml @@ -1,20 +1,15 @@ # Every resource the operator applies carries an ownerReference and a `.owns()` watch -# (main.rs); deleting it must trigger a reconcile of the KafkaCluster, which re-applies -# it. This step checks that chain — the ClusterRole's `watch` verbs plus the `.owns()` -# routing — and lives here because this test already cycles the pods. The `.watches()` -# registration (referenced-but-unowned ConfigMaps) can't be tested by deletion: the -# operator never recreates what it didn't apply. +# (main.rs): deleting it must trigger a reconcile of the KafkaCluster that re-applies it, +# proving the `.owns()` routing and the ClusterRole `watch` verbs end to end. +# `.watches()` registrations can't be tested this way: the operator never recreates +# what it didn't apply. # -# Recreation is proven by UID change: mere existence could pass without any deletion. -# TestStep commands run exactly once (no kuttl retry loop), so the polling stays quiet. -# -# The ConfigMap deleted here is the discovery one, named after the cluster. -# Deliberately skipped: the role-group ConfigMap (test-kafka-broker-default) — -# `.owns(ConfigMap)` is already exercised via the discovery ConfigMap, and deleting a -# ConfigMap mounted into running pods would disturb them. The bootstrap Listener is -# applied by this operator and therefore included, but its backing bootstrap Service -# and the per-broker pod Listeners are created by the listener-operator, so they are -# not part of this check. +# Resources are discovered by label (ClusterResources::add enforces the labels on +# everything the operator applies), so new resources and kinds are covered +# automatically. Labels over-match on derived objects, so each match must also carry +# a controller ownerReference pointing at the KafkaCluster; kinds that can never pass that +# gate are excluded up front. Recreation is proven by UID change, and a floor guard +# catches a selector that silently matches nothing. --- apiVersion: kuttl.dev/v1beta1 kind: TestStep @@ -26,28 +21,41 @@ commands: set -eu delete_and_await_recreation() { - kind=$1 - name=$2 - old_uid=$(kubectl get -n "$NAMESPACE" "$kind" "$name" -o jsonpath='{.metadata.uid}') - kubectl delete -n "$NAMESPACE" "$kind" "$name" --wait=false + resource=$1 + old_uid=$(kubectl get -n "$NAMESPACE" "$resource" -o jsonpath='{.metadata.uid}') + kubectl delete -n "$NAMESPACE" "$resource" --wait=false # Recreation is a single reconcile away, so this normally succeeds on the # first iteration; 30s is a generous upper bound well below the step timeout. for _ in $(seq 1 30); do - new_uid=$(kubectl get -n "$NAMESPACE" "$kind" "$name" -o jsonpath='{.metadata.uid}' 2>/dev/null || true) + new_uid=$(kubectl get -n "$NAMESPACE" "$resource" -o jsonpath='{.metadata.uid}' 2>/dev/null || true) if [ -n "$new_uid" ] && [ "$new_uid" != "$old_uid" ]; then return 0 fi sleep 1 done - echo "$kind/$name was not recreated (old uid: $old_uid, current: '${new_uid:-}')" >&2 + echo "$resource was not recreated (old uid: $old_uid, current: '${new_uid:-}')" >&2 return 1 } - delete_and_await_recreation serviceaccount test-kafka-serviceaccount - delete_and_await_recreation rolebinding test-kafka-rolebinding - delete_and_await_recreation poddisruptionbudget test-kafka-broker - delete_and_await_recreation listener test-kafka-broker-default-bootstrap - delete_and_await_recreation configmap test-kafka - delete_and_await_recreation service test-kafka-broker-default-headless - delete_and_await_recreation service test-kafka-broker-default-metrics - delete_and_await_recreation statefulset test-kafka-broker-default + selector="app.kubernetes.io/instance=test-kafka,app.kubernetes.io/managed-by=kafka.stackable.tech_kafkacluster" + excluded="^(pods|persistentvolumeclaims|endpoints|events)$|^endpointslices\.|^controllerrevisions\.|^events\." + + deleted=0 + for kind in $(kubectl api-resources --verbs=list --namespaced -o name | grep -Ev "$excluded" | sort); do + for resource in $(kubectl get -n "$NAMESPACE" "$kind" -l "$selector" -o name 2>/dev/null); do + owner=$(kubectl get -n "$NAMESPACE" "$resource" -o jsonpath='{.metadata.ownerReferences[?(@.controller==true)].kind}/{.metadata.ownerReferences[?(@.controller==true)].name}' 2>/dev/null || true) + if [ "$owner" != "KafkaCluster/test-kafka" ]; then + echo "skipping $resource: controller owner is '${owner:-none}', not the KafkaCluster" + continue + fi + delete_and_await_recreation "$resource" + deleted=$((deleted + 1)) + done + done + + # Guard against the sweep silently matching nothing (wrong selector, renamed + # labels): the fixture is known to produce well over this many owned resources. + if [ "$deleted" -lt 6 ]; then + echo "only $deleted labelled resources were swept - the label selector is broken" >&2 + exit 1 + fi From c35bf8eefe4d17f0314f0df60480ecaf8868d085 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Tue, 25 Aug 2026 10:49:18 +0200 Subject: [PATCH 4/4] use DeserializeGuard consistently --- rust/operator-binary/src/main.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index 16ec4f0d..8fa0210d 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -137,31 +137,32 @@ async fn main() -> anyhow::Result<()> { let config_map_store = kafka_controller.store(); let kafka_controller = kafka_controller .owns( - watch_namespace.get_api::(&client), + watch_namespace.get_api::>(&client), watcher::Config::default(), ) .owns( - watch_namespace.get_api::(&client), + watch_namespace + .get_api::>(&client), watcher::Config::default(), ) .owns( - watch_namespace.get_api::(&client), + watch_namespace.get_api::>(&client), watcher::Config::default(), ) .owns( - watch_namespace.get_api::(&client), + watch_namespace.get_api::>(&client), watcher::Config::default(), ) .owns( - watch_namespace.get_api::(&client), + watch_namespace.get_api::>(&client), watcher::Config::default(), ) .owns( - watch_namespace.get_api::(&client), + watch_namespace.get_api::>(&client), watcher::Config::default(), ) .owns( - watch_namespace.get_api::(&client), + watch_namespace.get_api::>(&client), watcher::Config::default(), ) .watches(