diff --git a/CHANGELOG.md b/CHANGELOG.md index 323d05c6..75204042 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ All notable changes to this project will be documented in this file. - Bump `stackable-operator` to 0.111.1 and snafu to 0.9 ([#960], [#961]). - Internal operator refactoring: introduce dereference() and validate() steps in the reconciler ([#968]). - test: Bump vector-aggregator to 0.55.0, replace /graphql call with gRPC call ([#971]). +- BREAKING: Removed product-config machinery which is a breaking change in terms of configuration. + Users relying on the product-config `properties.yaml` file have to set these properties via the CRD. + Config and environment overrides are now merged directly from the CRD into the validated cluster. + The `--product-config` CLI flag is now a no-op ([#976]). [#953]: https://github.com/stackabletech/kafka-operator/pull/953 [#960]: https://github.com/stackabletech/kafka-operator/pull/960 @@ -26,6 +30,7 @@ All notable changes to this project will be documented in this file. [#968]: https://github.com/stackabletech/kafka-operator/pull/968 [#971]: https://github.com/stackabletech/kafka-operator/pull/971 [#973]: https://github.com/stackabletech/kafka-operator/pull/973 +[#976]: https://github.com/stackabletech/kafka-operator/pull/976 ## [26.3.0] - 2026-03-16 diff --git a/Cargo.lock b/Cargo.lock index 5357bf1f..175dea75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,7 +141,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -152,7 +152,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -163,9 +163,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "axum" @@ -265,9 +265,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "block-buffer" @@ -280,9 +280,9 @@ dependencies = [ [[package]] name = "built" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" dependencies = [ "chrono", "git2", @@ -290,9 +290,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytes" @@ -302,9 +302,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.61" +version = "1.2.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" dependencies = [ "find-msvc-tools", "jobserver", @@ -320,9 +320,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "num-traits", @@ -360,7 +360,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -520,7 +520,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -531,7 +531,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -542,7 +542,7 @@ checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -566,7 +566,7 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -574,9 +574,6 @@ name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] [[package]] name = "derive_more" @@ -596,7 +593,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -613,13 +610,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -671,14 +668,14 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "elliptic-curve" @@ -735,7 +732,7 @@ checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -901,7 +898,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -918,9 +915,9 @@ checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" @@ -975,15 +972,14 @@ dependencies = [ [[package]] name = "git2" -version = "0.20.4" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" +checksum = "ddddbf932745a6be37109b6112d3ee09696106f848449069d3a57bba937ab82e" dependencies = [ "bitflags", "libc", "libgit2-sys", "log", - "url", ] [[package]] @@ -1017,9 +1013,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.13" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", @@ -1047,9 +1043,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -1079,9 +1075,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -1130,9 +1126,9 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -1328,9 +1324,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1343,7 +1339,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", ] [[package]] @@ -1361,16 +1357,6 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1405,9 +1391,9 @@ dependencies = [ [[package]] name = "jiff" -version = "0.2.24" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" +checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -1415,18 +1401,18 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-link", ] [[package]] name = "jiff-static" -version = "0.2.24" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" +checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1456,26 +1442,26 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] [[package]] name = "json-patch" -version = "4.1.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f300e415e2134745ef75f04562dd0145405c2f7fd92065db029ac4b16b57fe90" +checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72" dependencies = [ "jsonptr", + "schemars", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.18", ] [[package]] @@ -1517,11 +1503,11 @@ dependencies = [ [[package]] name = "k8s-version" version = "0.1.3" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#7a5f0c3fbcd091340214a23f0607fcd4b4fcc152" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "darling", "regex", - "snafu 0.9.0", + "snafu 0.9.1", ] [[package]] @@ -1617,7 +1603,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1664,9 +1650,9 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libgit2-sys" -version = "0.18.3+1.9.2" +version = "0.18.5+1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" +checksum = "005d6ae6eac1912906073e069f7db60b1fa98e052a68227824afe3e3a1c59ca2" dependencies = [ "cc", "libc", @@ -1682,9 +1668,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libz-sys" -version = "1.1.28" +version = "1.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" dependencies = [ "cc", "libc", @@ -1709,9 +1695,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" [[package]] name = "matchers" @@ -1730,9 +1716,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "mime" @@ -1752,9 +1738,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", @@ -1788,9 +1774,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" @@ -1842,9 +1828,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "opentelemetry" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" +checksum = "b0142c63252a9e054e68a4c61a5778f7b14f576274d593f8ce883d191a099682" dependencies = [ "futures-core", "futures-sink", @@ -1856,9 +1842,9 @@ dependencies = [ [[package]] name = "opentelemetry-appender-tracing" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef6a1ac5ca3accf562b8c306fa8483c85f4390f768185ab775f242f7fe8fdcc2" +checksum = "2c0080f0dc1d7c786f467cd85a4e395fcab11ee852004f39a29a18ab7c25d837" dependencies = [ "opentelemetry", "tracing", @@ -1868,9 +1854,9 @@ dependencies = [ [[package]] name = "opentelemetry-http" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" +checksum = "5683015d09e2df236ef005b17f6f196f0d5f6313c4fa43a7b6a53b52776e4331" dependencies = [ "async-trait", "bytes", @@ -1881,9 +1867,9 @@ dependencies = [ [[package]] name = "opentelemetry-otlp" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f" +checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35" dependencies = [ "http", "opentelemetry", @@ -1895,14 +1881,14 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tonic", - "tracing", + "tonic-types", ] [[package]] name = "opentelemetry-proto" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" +checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638" dependencies = [ "opentelemetry", "opentelemetry_sdk", @@ -1913,21 +1899,22 @@ dependencies = [ [[package]] name = "opentelemetry-semantic-conventions" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e62e29dfe041afb8ed2a6c9737ab57db4907285d999ef8ad3a59092a36bdc846" +checksum = "6ca2f98a0437b427b4b08f19f1caa3c44db885a202bc12cfea13d6c702243d68" [[package]] name = "opentelemetry_sdk" -version = "0.31.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" +checksum = "9b59f80e1ac4d5ff7a2db8fb6c80badb7f0f3f858211fba08dd9aaec750894f9" dependencies = [ "futures-channel", "futures-executor", "futures-util", "opentelemetry", "percent-encoding", + "portable-atomic", "rand 0.9.4", "thiserror 2.0.18", "tokio", @@ -2039,7 +2026,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2054,22 +2041,22 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2189,9 +2176,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" dependencies = [ "bytes", "prost-derive", @@ -2199,15 +2186,24 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", "itertools", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", +] + +[[package]] +name = "prost-types" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" +dependencies = [ + "prost", ] [[package]] @@ -2309,14 +2305,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -2337,9 +2333,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "relative-path" @@ -2349,9 +2345,9 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "reqwest" -version = "0.12.28" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", @@ -2367,9 +2363,6 @@ dependencies = [ "log", "percent-encoding", "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", "sync_wrapper", "tokio", "tower", @@ -2451,7 +2444,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.117", + "syn 2.0.118", "unicode-ident", ] @@ -2466,9 +2459,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.39" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "log", "once_cell", @@ -2481,9 +2474,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -2555,7 +2548,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2653,7 +2646,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2664,14 +2657,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -2749,9 +2742,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook-registry" @@ -2787,9 +2780,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "snafu" @@ -2812,11 +2805,11 @@ dependencies = [ [[package]] name = "snafu" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1d4bced6a69f90b2056c03dcff2c4737f98d6fb9e0853493996e1d253ca29c6" +checksum = "d1a012328be2e3f5d5f6f3218147ca02588cea4cb865e876849ab6debcf36522" dependencies = [ - "snafu-derive 0.9.0", + "snafu-derive 0.9.1", ] [[package]] @@ -2839,26 +2832,26 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "snafu-derive" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54254b8531cafa275c5e096f62d48c81435d1015405a91198ddb11e967301d40" +checksum = "5f103c50866b8743da9429b8a581d81a27c2d3a9c4ac7df8f8571c1dd7896eda" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -2889,7 +2882,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stackable-certs" version = "0.4.0" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#7a5f0c3fbcd091340214a23f0607fcd4b4fcc152" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "const-oid", "ecdsa", @@ -2901,7 +2894,7 @@ dependencies = [ "rsa", "sha2", "signature", - "snafu 0.9.0", + "snafu 0.9.1", "stackable-shared", "tokio", "tokio-rustls", @@ -2920,12 +2913,11 @@ dependencies = [ "const_format", "futures", "indoc", - "product-config", "rstest", "serde", "serde_json", "serde_yaml", - "snafu 0.9.0", + "snafu 0.9.1", "stackable-operator", "strum", "tokio", @@ -2935,7 +2927,7 @@ dependencies = [ [[package]] name = "stackable-operator" version = "0.111.1" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#7a5f0c3fbcd091340214a23f0607fcd4b4fcc152" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "base64", "clap", @@ -2947,6 +2939,7 @@ dependencies = [ "futures", "http", "indexmap", + "java-properties", "jiff", "json-patch", "k8s-openapi", @@ -2959,7 +2952,8 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "snafu 0.9.0", + "sha2", + "snafu 0.9.1", "stackable-operator-derive", "stackable-shared", "stackable-telemetry", @@ -2971,23 +2965,25 @@ dependencies = [ "tracing-appender", "tracing-subscriber", "url", + "uuid", + "xml", ] [[package]] name = "stackable-operator-derive" version = "0.3.1" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#7a5f0c3fbcd091340214a23f0607fcd4b4fcc152" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "stackable-shared" -version = "0.1.0" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#7a5f0c3fbcd091340214a23f0607fcd4b4fcc152" +version = "0.1.1" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "jiff", "k8s-openapi", @@ -2996,15 +2992,15 @@ dependencies = [ "semver", "serde", "serde_yaml", - "snafu 0.9.0", + "snafu 0.9.1", "strum", "time", ] [[package]] name = "stackable-telemetry" -version = "0.6.3" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#7a5f0c3fbcd091340214a23f0607fcd4b4fcc152" +version = "0.6.4" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "axum", "clap", @@ -3015,7 +3011,7 @@ dependencies = [ "opentelemetry-semantic-conventions", "opentelemetry_sdk", "pin-project", - "snafu 0.9.0", + "snafu 0.9.1", "strum", "tokio", "tower", @@ -3028,21 +3024,21 @@ dependencies = [ [[package]] name = "stackable-versioned" version = "0.10.0" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#7a5f0c3fbcd091340214a23f0607fcd4b4fcc152" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "kube", "schemars", "serde", "serde_json", "serde_yaml", - "snafu 0.9.0", + "snafu 0.9.1", "stackable-versioned-macros", ] [[package]] name = "stackable-versioned-macros" version = "0.10.0" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#7a5f0c3fbcd091340214a23f0607fcd4b4fcc152" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "convert_case", "convert_case_extras", @@ -3054,13 +3050,13 @@ dependencies = [ "kube", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "stackable-webhook" version = "0.9.1" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#7a5f0c3fbcd091340214a23f0607fcd4b4fcc152" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "arc-swap", "async-trait", @@ -3076,7 +3072,7 @@ dependencies = [ "rand 0.9.4", "serde", "serde_json", - "snafu 0.9.0", + "snafu 0.9.1", "stackable-certs", "stackable-shared", "stackable-telemetry", @@ -3113,7 +3109,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3141,9 +3137,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -3167,7 +3163,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3196,7 +3192,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3207,7 +3203,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3221,12 +3217,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde_core", @@ -3236,15 +3231,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d" dependencies = [ "num-conv", "time-core", @@ -3278,14 +3273,14 @@ checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "tokio" -version = "1.52.1" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -3306,7 +3301,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3355,9 +3350,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.11+spec-1.1.0" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ "indexmap", "toml_datetime", @@ -3376,9 +3371,9 @@ dependencies = [ [[package]] name = "tonic" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" dependencies = [ "async-trait", "base64", @@ -3403,15 +3398,26 @@ dependencies = [ [[package]] name = "tonic-prost" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" dependencies = [ "bytes", "prost", "tonic", ] +[[package]] +name = "tonic-types" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab1b02061f83d519bba3caa167f88f261ef05720ab8ebc954ade70de3348e8" +dependencies = [ + "prost", + "prost-types", + "tonic", +] + [[package]] name = "tower" version = "0.5.3" @@ -3433,9 +3439,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "base64", "bitflags", @@ -3443,13 +3449,13 @@ dependencies = [ "futures-util", "http", "http-body", - "iri-string", "mime", "pin-project-lite", "tower", "tower-layer", "tower-service", "tracing", + "url", ] [[package]] @@ -3497,7 +3503,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3523,9 +3529,9 @@ dependencies = [ [[package]] name = "tracing-opentelemetry" -version = "0.32.1" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc" +checksum = "adbc64cba7137545b8044cb1fe9814f7aacf3c6b5f9b45be8bb5db538befdb26" dependencies = [ "js-sys", "opentelemetry", @@ -3576,9 +3582,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ucd-trie" @@ -3594,9 +3600,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-xid" @@ -3641,6 +3647,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -3676,18 +3692,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" dependencies = [ "cfg-if", "once_cell", @@ -3698,9 +3714,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.68" +version = "0.4.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" dependencies = [ "js-sys", "wasm-bindgen", @@ -3708,9 +3724,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3718,31 +3734,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.95" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" dependencies = [ "js-sys", "wasm-bindgen", @@ -3779,7 +3795,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3790,7 +3806,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3901,9 +3917,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] @@ -3936,15 +3952,15 @@ dependencies = [ [[package]] name = "xml" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8aa498d22c9bbaf482329839bc5620c46be275a19a812e9a22a2b07529a642a" +checksum = "636f85e5ca6488e96401b61eb7de54f4e44755c988af0f52cf90230c312a1a89" [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -3959,35 +3975,35 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] @@ -4000,28 +4016,28 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4054,7 +4070,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] diff --git a/Cargo.nix b/Cargo.nix index 73891047..14477171 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -439,7 +439,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" "visit-mut" ]; } ]; @@ -466,7 +466,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; usesDefaultFeatures = false; features = [ "clone-impls" "full" "parsing" "printing" "proc-macro" "visit-mut" ]; } @@ -489,9 +489,9 @@ rec { }; "autocfg" = rec { crateName = "autocfg"; - version = "1.5.0"; + version = "1.5.1"; edition = "2015"; - sha256 = "1s77f98id9l4af4alklmzq46f21c980v13z2r1pcxx6bqgw0d1n0"; + sha256 = "0lqasy5i30flcgih1b50kvsk6z32g09r1q4ql7q81pj6228jy0zj"; authors = [ "Josh Stone " ]; @@ -866,9 +866,9 @@ rec { }; "bitflags" = rec { crateName = "bitflags"; - version = "2.11.1"; + version = "2.13.0"; edition = "2021"; - sha256 = "1cvqijg3rvwgis20a66vfdxannjsxfy5fgjqkaq3l13gyfcj4lf4"; + sha256 = "1y239gpvl061rfvav7jds8mjs42kmwi39is7yx5d1qw3hvp8nf5l"; authors = [ "The Rust Project Developers" ]; @@ -898,9 +898,9 @@ rec { }; "built" = rec { crateName = "built"; - version = "0.8.0"; - edition = "2021"; - sha256 = "0r5f08lpjsr6j5ajkbmd0ymfmajpq8ddbfvi8ji8rx48y88qzbgl"; + version = "0.8.1"; + edition = "2024"; + sha256 = "1saq332pd6g3svvc9ah8myjpfvgqlzl2ksb1ypp3976kjcfm63jw"; authors = [ "Lukas Lueg " ]; @@ -924,15 +924,16 @@ rec { "chrono" = [ "dep:chrono" ]; "dependency-tree" = [ "cargo-lock/dependency-tree" ]; "git2" = [ "dep:git2" ]; + "gix" = [ "dep:gix" ]; "semver" = [ "dep:semver" ]; }; resolvedDefaultFeatures = [ "chrono" "git2" ]; }; "bumpalo" = rec { crateName = "bumpalo"; - version = "3.20.2"; + version = "3.20.3"; edition = "2021"; - sha256 = "1jrgxlff76k9glam0akhwpil2fr1w32gbjdf5hpipc7ld2c7h82x"; + sha256 = "0jc6va3nwcqikm7chnpdv1s87my3gs2j7g1sc7g3k91brg3arxbj"; authors = [ "Nick Fitzgerald " ]; @@ -961,9 +962,9 @@ rec { }; "cc" = rec { crateName = "cc"; - version = "1.2.61"; + version = "1.2.64"; edition = "2018"; - sha256 = "0vawvnrrsmi8dygavq3wx085cmlp10sp3fhld5842rlqkqsr0vfi"; + sha256 = "07shcd8faxw7csz13m3cg2mj6i8z07pqs960k181pscbjpyqgn6s"; authors = [ "Alex Crichton " ]; @@ -1011,9 +1012,9 @@ rec { }; "chrono" = rec { crateName = "chrono"; - version = "0.4.44"; + version = "0.4.45"; edition = "2021"; - sha256 = "1c64mk9a235271j5g3v4zrzqqmd43vp9vki7vqfllpqf5rd0fwy6"; + sha256 = "09rkcgk6is2sdhqs9142zv8xqnj8ryx8m9hknllqwyv9wxi9x9qs"; dependencies = [ { name = "iana-time-zone"; @@ -1160,7 +1161,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" ]; } ]; @@ -1591,7 +1592,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" "extra-traits" ]; } ]; @@ -1622,7 +1623,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; @@ -1648,7 +1649,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" "visit-mut" ]; } ]; @@ -1725,7 +1726,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "extra-traits" ]; } ]; @@ -1739,14 +1740,6 @@ rec { authors = [ "Jacob Pratt " ]; - dependencies = [ - { - name = "powerfmt"; - packageId = "powerfmt"; - optional = true; - usesDefaultFeatures = false; - } - ]; features = { "macros" = [ "dep:deranged-macros" ]; "num" = [ "dep:num-traits" ]; @@ -1758,7 +1751,7 @@ rec { "rand09" = [ "dep:rand09" ]; "serde" = [ "dep:serde_core" ]; }; - resolvedDefaultFeatures = [ "default" "powerfmt" ]; + resolvedDefaultFeatures = [ "default" ]; }; "derive_more" = rec { crateName = "derive_more"; @@ -1827,7 +1820,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; buildDependencies = [ @@ -1906,9 +1899,9 @@ rec { }; "displaydoc" = rec { crateName = "displaydoc"; - version = "0.2.5"; + version = "0.2.6"; edition = "2021"; - sha256 = "1q0alair462j21iiqwrr21iabkfnb13d6x5w95lkdg21q2xrqdlp"; + sha256 = "0kyxwfbdmagd8afzb2pzja7wj8dhah7smxdsgw00iq8pa2jhmiqs"; procMacro = true; authors = [ "Jane Lusby " @@ -1924,7 +1917,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; features = { @@ -2090,13 +2083,13 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; devDependencies = [ { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" ]; } ]; @@ -2108,12 +2101,9 @@ rec { }; "either" = rec { crateName = "either"; - version = "1.15.0"; + version = "1.16.0"; edition = "2021"; - sha256 = "069p1fknsmzn9llaizh77kip0pqmcwpdsykv2x30xpjyija5gis8"; - authors = [ - "bluss" - ]; + sha256 = "17k7jfbdz7k440h6lws9baz8p9zlxgb41sig3w81h80nwzsjyqli"; features = { "default" = [ "std" ]; "serde" = [ "dep:serde" ]; @@ -2297,7 +2287,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; features = { @@ -2785,7 +2775,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" ]; } ]; @@ -2817,9 +2807,9 @@ rec { }; "futures-timer" = rec { crateName = "futures-timer"; - version = "3.0.3"; + version = "3.0.4"; edition = "2018"; - sha256 = "094vw8k37djpbwv74bwf2qb7n6v6ghif4myss6smd6hgyajb127j"; + sha256 = "0s39in8ivw7g4d37pf31q02y44zd1hpfkd1pgra2slcqibdzlhxg"; libName = "futures_timer"; authors = [ "Alex Crichton " @@ -3066,9 +3056,9 @@ rec { }; "git2" = rec { crateName = "git2"; - version = "0.20.4"; - edition = "2018"; - sha256 = "0azykjpk3j6s354z23jkyq3r3pbmlw9ha1zsxkw5cnnpi1h2b23v"; + version = "0.21.0"; + edition = "2021"; + sha256 = "0bmqga9vlyx5sdlr0i28z0362s89xv9i4qcv20vvx9j54y9vzpfx"; authors = [ "Josh Triplett " "Alex Crichton " @@ -3090,17 +3080,14 @@ rec { name = "log"; packageId = "log"; } - { - name = "url"; - packageId = "url"; - } ]; features = { - "default" = [ "ssh" "https" ]; - "https" = [ "libgit2-sys/https" "openssl-sys" "openssl-probe" ]; + "cred" = [ "dep:url" ]; + "https" = [ "libgit2-sys/https" "openssl-sys" "openssl-probe" "cred" ]; "openssl-probe" = [ "dep:openssl-probe" ]; "openssl-sys" = [ "dep:openssl-sys" ]; - "ssh" = [ "libgit2-sys/ssh" ]; + "ssh" = [ "libgit2-sys/ssh" "cred" ]; + "unstable-sha256" = [ "libgit2-sys/unstable-sha256" ]; "vendored-libgit2" = [ "libgit2-sys/vendored" ]; "vendored-openssl" = [ "openssl-sys/vendored" "libgit2-sys/vendored-openssl" ]; "zlib-ng-compat" = [ "libgit2-sys/zlib-ng-compat" ]; @@ -3190,9 +3177,9 @@ rec { }; "h2" = rec { crateName = "h2"; - version = "0.4.13"; + version = "0.4.15"; edition = "2021"; - sha256 = "0m6w5gg0n0m1m5915bxrv8n4rlazhx5icknkslz719jhh4xdli1g"; + sha256 = "0mgilh1g8gydcchqi6acs5l6j0gwg5jwpa64sj4b3ncb9v497c3c"; authors = [ "Carl Lerche " "Sean McArthur " @@ -3303,14 +3290,11 @@ rec { }; resolvedDefaultFeatures = [ "allocator-api2" "default" "default-hasher" "equivalent" "inline-more" "raw-entry" ]; }; - "hashbrown 0.17.0" = rec { + "hashbrown 0.17.1" = rec { crateName = "hashbrown"; - version = "0.17.0"; + version = "0.17.1"; edition = "2024"; - sha256 = "0l8gvcz80lvinb7x22h53cqbi2y1fm603y2jhhh9qwygvkb7sijg"; - authors = [ - "Amanieu d'Antras " - ]; + sha256 = "0jmqz7i4yl6cm7rbn0i2ffkfrmwi6xkmzkaldr2v8bcsx2v0jngd"; features = { "alloc" = [ "dep:alloc" ]; "allocator-api2" = [ "dep:allocator-api2" ]; @@ -3385,9 +3369,9 @@ rec { }; "http" = rec { crateName = "http"; - version = "1.4.0"; + version = "1.4.2"; edition = "2021"; - sha256 = "06iind4cwsj1d6q8c2xgq8i2wka4ps74kmws24gsi1bzdlw2mfp3"; + sha256 = "09b4p8fiivkg7wm0b59fyrn1jkm7px298ci7zb9igz6n647gaw39"; authors = [ "Alex Crichton " "Carl Lerche " @@ -3504,9 +3488,9 @@ rec { }; "hyper" = rec { crateName = "hyper"; - version = "1.9.0"; + version = "1.10.1"; edition = "2021"; - sha256 = "1jmwbwqcaficskg76kq402gbymbnh2z4v99xwq3l5aa6n8bg16b2"; + sha256 = "1624nwrh1ci34psqcl3q8q266kha8kd6fmqjj14qck49l59iqa2m"; authors = [ "Sean McArthur " ]; @@ -4277,9 +4261,9 @@ rec { }; "idna_adapter" = rec { crateName = "idna_adapter"; - version = "1.2.1"; - edition = "2021"; - sha256 = "0i0339pxig6mv786nkqcxnwqa87v4m94b2653f6k3aj0jmhfkjis"; + version = "1.2.2"; + edition = "2024"; + sha256 = "0557p76l8hj35r9zn1yv7c6x1c0qbrsffmg80n0yy8361ly3fs6b"; authors = [ "The rust-url developers" ]; @@ -4313,7 +4297,7 @@ rec { } { name = "hashbrown"; - packageId = "hashbrown 0.17.0"; + packageId = "hashbrown 0.17.1"; usesDefaultFeatures = false; } ]; @@ -4371,39 +4355,6 @@ rec { }; resolvedDefaultFeatures = [ "default" "std" ]; }; - "iri-string" = rec { - crateName = "iri-string"; - version = "0.7.12"; - edition = "2021"; - sha256 = "082fpx6c5ghvmqpwxaf2b268m47z2ic3prajqbmi1s1qpfj5kri5"; - libName = "iri_string"; - authors = [ - "YOSHIOKA Takuma " - ]; - dependencies = [ - { - name = "memchr"; - packageId = "memchr"; - optional = true; - usesDefaultFeatures = false; - } - { - name = "serde"; - packageId = "serde"; - optional = true; - usesDefaultFeatures = false; - features = [ "derive" ]; - } - ]; - features = { - "alloc" = [ "serde?/alloc" ]; - "default" = [ "std" ]; - "memchr" = [ "dep:memchr" ]; - "serde" = [ "dep:serde" ]; - "std" = [ "alloc" "memchr?/std" "serde?/std" ]; - }; - resolvedDefaultFeatures = [ "alloc" "default" "std" ]; - }; "is_terminal_polyfill" = rec { crateName = "is_terminal_polyfill"; version = "1.70.2"; @@ -4473,9 +4424,9 @@ rec { }; "jiff" = rec { crateName = "jiff"; - version = "0.2.24"; + version = "0.2.28"; edition = "2021"; - sha256 = "0g87al8yqp05m63dhqzi359xgsslc0grqz00nvfdyq8dcayms2zh"; + sha256 = "00lixngcc7amh2fcsxfr0z38j06lllhapz192biv1qj97q1x60s6"; authors = [ "Andrew Gallant " ]; @@ -4521,12 +4472,10 @@ rec { usesDefaultFeatures = false; } { - name = "windows-sys"; - packageId = "windows-sys 0.61.2"; + name = "windows-link"; + packageId = "windows-link"; optional = true; - usesDefaultFeatures = false; target = { target, features }: (target."windows" or false); - features = [ "Win32_Foundation" "Win32_System_Time" ]; } ]; devDependencies = [ @@ -4545,7 +4494,7 @@ rec { "static-tz" = [ "dep:jiff-static" ]; "std" = [ "alloc" "log?/std" "serde_core?/std" ]; "tz-fat" = [ "jiff-static?/tz-fat" ]; - "tz-system" = [ "std" "dep:windows-sys" ]; + "tz-system" = [ "std" "dep:windows-link" ]; "tzdb-bundle-always" = [ "dep:jiff-tzdb" "alloc" ]; "tzdb-bundle-platform" = [ "dep:jiff-tzdb-platform" "alloc" ]; "tzdb-concatenated" = [ "std" ]; @@ -4555,9 +4504,9 @@ rec { }; "jiff-static" = rec { crateName = "jiff-static"; - version = "0.2.24"; + version = "0.2.28"; edition = "2021"; - sha256 = "1mz6v0d1hd8wjgfzccgda5g9z01s1yxnyiizvahjw0pq1w1xw070"; + sha256 = "0irbhfh2f4i9w5l53jcmh6ssnhdd92wfy76978chgwnxilvk4bbq"; procMacro = true; libName = "jiff_static"; authors = [ @@ -4574,7 +4523,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; features = { @@ -4637,9 +4586,9 @@ rec { }; "js-sys" = rec { crateName = "js-sys"; - version = "0.3.95"; + version = "0.3.102"; edition = "2021"; - sha256 = "1jhj3kgxxgwm0cpdjiz7i2qapqr7ya9qswadmr63dhwx3lnyjr19"; + sha256 = "0cgxklnyrfpzvf32cvdl3x5d070kfsv7ykdxfl3yizwdjqq4rl03"; libName = "js_sys"; authors = [ "The wasm-bindgen Developers" @@ -4648,7 +4597,6 @@ rec { { name = "cfg-if"; packageId = "cfg-if"; - optional = true; } { name = "futures-util"; @@ -4657,11 +4605,6 @@ rec { usesDefaultFeatures = false; features = [ "std" ]; } - { - name = "once_cell"; - packageId = "once_cell"; - usesDefaultFeatures = false; - } { name = "wasm-bindgen"; packageId = "wasm-bindgen"; @@ -4670,17 +4613,16 @@ rec { ]; features = { "default" = [ "std" "unsafe-eval" ]; - "futures" = [ "dep:cfg-if" "dep:futures-util" ]; - "futures-core-03-stream" = [ "futures" "dep:futures-core" ]; - "std" = [ "wasm-bindgen/std" ]; + "futures-core-03-stream" = [ "dep:futures-util" "dep:futures-core" ]; + "std" = [ "wasm-bindgen/std" "dep:futures-util" ]; }; - resolvedDefaultFeatures = [ "default" "futures" "std" "unsafe-eval" ]; + resolvedDefaultFeatures = [ "default" "std" "unsafe-eval" ]; }; "json-patch" = rec { crateName = "json-patch"; - version = "4.1.0"; + version = "4.2.0"; edition = "2021"; - sha256 = "147yaxmv3i4s0bdna86rgwpmqh2507fn4ighfpplaiqkw8ay807k"; + sha256 = "0wkv896d0pzq56i2kkl0giqpv117fwvhbpgs8iz85805w66l68bl"; libName = "json_patch"; authors = [ "Ivan Dubrov " @@ -4690,6 +4632,11 @@ rec { name = "jsonptr"; packageId = "jsonptr"; } + { + name = "schemars"; + packageId = "schemars"; + optional = true; + } { name = "serde"; packageId = "serde"; @@ -4701,10 +4648,14 @@ rec { } { name = "thiserror"; - packageId = "thiserror 1.0.69"; + packageId = "thiserror 2.0.18"; } ]; devDependencies = [ + { + name = "schemars"; + packageId = "schemars"; + } { name = "serde_json"; packageId = "serde_json"; @@ -4716,7 +4667,7 @@ rec { "schemars" = [ "dep:schemars" ]; "utoipa" = [ "dep:utoipa" ]; }; - resolvedDefaultFeatures = [ "default" "diff" ]; + resolvedDefaultFeatures = [ "default" "diff" "schemars" ]; }; "jsonpath-rust" = rec { crateName = "jsonpath-rust"; @@ -4842,9 +4793,9 @@ rec { edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; libName = "k8s_version"; authors = [ @@ -4862,7 +4813,7 @@ rec { } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } ]; features = { @@ -5337,7 +5288,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "extra-traits" ]; } ]; @@ -5510,10 +5461,10 @@ rec { }; "libgit2-sys" = rec { crateName = "libgit2-sys"; - version = "0.18.3+1.9.2"; + version = "0.18.5+1.9.4"; edition = "2021"; links = "git2"; - sha256 = "11rlbyihj3k35mnkxxz4yvsnlx33a4r9srl66c5vp08pp72arcy9"; + sha256 = "18lwqnhy7qxg4iw24s1a0n7aj7qbnryry1iy0w32k4f1xbk6lp80"; libName = "libgit2_sys"; libPath = "lib.rs"; authors = [ @@ -5571,10 +5522,10 @@ rec { }; "libz-sys" = rec { crateName = "libz-sys"; - version = "1.1.28"; + version = "1.1.29"; edition = "2018"; links = "z"; - sha256 = "08hyf9v85zifl3353xc7i5wr53v9b3scri856cmphl3gaxp24fpw"; + sha256 = "1n98kqya7a7a0cxf5n5z3g13rj7a1vqxynk2xc7bja1qfxbrdg45"; libName = "libz_sys"; authors = [ "Alex Crichton " @@ -5651,9 +5602,9 @@ rec { }; "log" = rec { crateName = "log"; - version = "0.4.29"; + version = "0.4.32"; edition = "2021"; - sha256 = "15q8j9c8g5zpkcw0hnd6cf2z7fxqnvsjh3rw5mv5q10r83i34l2y"; + sha256 = "0fmdg0cxig7i4fwf1sw7fmg4d1gdbfzniawcfpwydy1q7320fgwm"; authors = [ "The Rust Project Developers" ]; @@ -5707,9 +5658,9 @@ rec { }; "memchr" = rec { crateName = "memchr"; - version = "2.8.0"; + version = "2.8.2"; edition = "2021"; - sha256 = "0y9zzxcqxvdqg6wyag7vc3h0blhdn7hkq164bxyx2vph8zs5ijpq"; + sha256 = "1i33wr49pcz2sbd12nds3n9fszay8kq5bk78gwciz462mcs49448"; authors = [ "Andrew Gallant " "bluss" @@ -5770,9 +5721,9 @@ rec { }; "mio" = rec { crateName = "mio"; - version = "1.2.0"; + version = "1.2.1"; edition = "2021"; - sha256 = "1hanrh4fwsfkdqdaqfidz48zz1wdix23zwn3r2x78am0garfbdsh"; + sha256 = "1nkggmrlnjs93w8rja4lvjj4aml1xqahgimv1h0p7d373kvhmg82"; authors = [ "Carl Lerche " "Thomas de Zeeuw " @@ -5908,9 +5859,9 @@ rec { }; "num-conv" = rec { crateName = "num-conv"; - version = "0.2.1"; + version = "0.2.2"; edition = "2021"; - sha256 = "0rqrr29brafaa2za352pbmhkk556n7f8z9rrkgmjp1idvdl3fry6"; + sha256 = "0hg4f9bwmy7cwpxdkm165dmkfc8jhkkayci234jsmi5ssb33j5sj"; libName = "num_conv"; authors = [ "Jacob Pratt " @@ -6043,9 +5994,9 @@ rec { }; "opentelemetry" = rec { crateName = "opentelemetry"; - version = "0.31.0"; + version = "0.32.0"; edition = "2021"; - sha256 = "18629xsj4rsyiby9aj511q6wcw6s9m09gx3ymw1yjcvix1mcsjxq"; + sha256 = "10ln14d1jgc8rvw97mblc9blzcgpg1bimim4d170b7ia4mijq55h"; dependencies = [ { name = "futures-core"; @@ -6082,24 +6033,24 @@ rec { ]; features = { "default" = [ "trace" "metrics" "logs" "internal-logs" "futures" ]; + "experimental_metrics_bound_instruments" = [ "metrics" ]; "futures" = [ "futures-core" "futures-sink" "pin-project-lite" ]; "futures-core" = [ "dep:futures-core" ]; "futures-sink" = [ "dep:futures-sink" ]; "internal-logs" = [ "tracing" ]; "pin-project-lite" = [ "dep:pin-project-lite" ]; - "spec_unstable_logs_enabled" = [ "logs" ]; "testing" = [ "trace" ]; "thiserror" = [ "dep:thiserror" ]; "trace" = [ "futures" "thiserror" ]; "tracing" = [ "dep:tracing" ]; }; - resolvedDefaultFeatures = [ "default" "futures" "futures-core" "futures-sink" "internal-logs" "logs" "metrics" "pin-project-lite" "spec_unstable_logs_enabled" "thiserror" "trace" "tracing" ]; + resolvedDefaultFeatures = [ "default" "futures" "futures-core" "futures-sink" "internal-logs" "logs" "metrics" "pin-project-lite" "thiserror" "trace" "tracing" ]; }; "opentelemetry-appender-tracing" = rec { crateName = "opentelemetry-appender-tracing"; - version = "0.31.1"; + version = "0.32.0"; edition = "2021"; - sha256 = "1hnwizzgfhpjfnvml638yy846py8hf2gl1n3p1igbk1srb2ilspg"; + sha256 = "0dyq4myan64sl8wly02jx0gb3jjz7575mn3w8rpphz0xvkq8001c"; libName = "opentelemetry_appender_tracing"; dependencies = [ { @@ -6142,18 +6093,15 @@ rec { ]; features = { "experimental_metadata_attributes" = [ "dep:tracing-log" ]; - "experimental_use_tracing_span_context" = [ "tracing-opentelemetry" ]; "log" = [ "dep:log" ]; - "spec_unstable_logs_enabled" = [ "opentelemetry/spec_unstable_logs_enabled" ]; - "tracing-opentelemetry" = [ "dep:tracing-opentelemetry" ]; }; resolvedDefaultFeatures = [ "default" ]; }; "opentelemetry-http" = rec { crateName = "opentelemetry-http"; - version = "0.31.0"; + version = "0.32.0"; edition = "2021"; - sha256 = "0pc5nw1ds8v8w0nvyall39m92v8m1xl1p3vwvxk6nkhrffdd19np"; + sha256 = "0ca3drvm4fx5nskl7yn42dimy3bg35ppzc85y1p27pz215fh30sn"; libName = "opentelemetry_http"; dependencies = [ { @@ -6189,16 +6137,16 @@ rec { "internal-logs" = [ "opentelemetry/internal-logs" ]; "reqwest" = [ "dep:reqwest" ]; "reqwest-blocking" = [ "dep:reqwest" "reqwest/blocking" ]; - "reqwest-rustls" = [ "dep:reqwest" "reqwest/rustls-tls-native-roots" ]; - "reqwest-rustls-webpki-roots" = [ "dep:reqwest" "reqwest/rustls-tls-webpki-roots" ]; + "reqwest-rustls" = [ "dep:reqwest" "reqwest/default-tls" ]; + "reqwest-rustls-webpki-roots" = [ "dep:reqwest" "reqwest/default-tls" "reqwest/webpki-roots" ]; }; - resolvedDefaultFeatures = [ "internal-logs" "reqwest" "reqwest-blocking" ]; + resolvedDefaultFeatures = [ "reqwest" "reqwest-blocking" ]; }; "opentelemetry-otlp" = rec { crateName = "opentelemetry-otlp"; - version = "0.31.1"; + version = "0.32.0"; edition = "2021"; - sha256 = "07zp0b62b9dajnvvcd6j2ppw5zg7wp4ixka9z6fr3bxrrdmcss8z"; + sha256 = "0d9cys2flpidfxbr6h1103hjc633cax47ihnqgbj0xnicscr4rlr"; libName = "opentelemetry_otlp"; dependencies = [ { @@ -6259,10 +6207,9 @@ rec { usesDefaultFeatures = false; } { - name = "tracing"; - packageId = "tracing"; + name = "tonic-types"; + packageId = "tonic-types"; optional = true; - usesDefaultFeatures = false; } ]; devDependencies = [ @@ -6287,16 +6234,19 @@ rec { ]; features = { "default" = [ "http-proto" "reqwest-blocking-client" "trace" "metrics" "logs" "internal-logs" ]; + "experimental-grpc-retry" = [ "grpc-tonic" "opentelemetry_sdk/experimental_async_runtime" "opentelemetry_sdk/rt-tokio" ]; + "experimental-http-retry" = [ "opentelemetry_sdk/experimental_async_runtime" "opentelemetry_sdk/rt-tokio" "tokio" "httpdate" ]; "flate2" = [ "dep:flate2" ]; - "grpc-tonic" = [ "tonic" "prost" "http" "tokio" "opentelemetry-proto/gen-tonic" ]; + "grpc-tonic" = [ "tonic" "tonic-types" "prost" "http" "tokio" "opentelemetry-proto/gen-tonic" ]; "gzip-http" = [ "flate2" ]; "gzip-tonic" = [ "tonic/gzip" ]; "http" = [ "dep:http" ]; "http-json" = [ "serde_json" "prost" "opentelemetry-http" "opentelemetry-proto/gen-tonic-messages" "opentelemetry-proto/with-serde" "http" "trace" "metrics" ]; "http-proto" = [ "prost" "opentelemetry-http" "opentelemetry-proto/gen-tonic-messages" "http" "trace" "metrics" ]; + "httpdate" = [ "dep:httpdate" ]; "hyper-client" = [ "opentelemetry-http/hyper" ]; "integration-testing" = [ "tonic" "prost" "tokio/full" "trace" "logs" ]; - "internal-logs" = [ "tracing" "opentelemetry_sdk/internal-logs" "opentelemetry-http/internal-logs" ]; + "internal-logs" = [ "opentelemetry_sdk/internal-logs" "opentelemetry/internal-logs" ]; "logs" = [ "opentelemetry/logs" "opentelemetry_sdk/logs" "opentelemetry-proto/logs" ]; "metrics" = [ "opentelemetry/metrics" "opentelemetry_sdk/metrics" "opentelemetry-proto/metrics" ]; "opentelemetry-http" = [ "dep:opentelemetry-http" ]; @@ -6309,27 +6259,27 @@ rec { "serde" = [ "dep:serde" ]; "serde_json" = [ "dep:serde_json" ]; "serialize" = [ "serde" "serde_json" ]; - "tls" = [ "tonic/tls-ring" ]; + "tls" = [ "tls-ring" ]; "tls-aws-lc" = [ "tonic/tls-aws-lc" ]; "tls-provider-agnostic" = [ "tonic/_tls-any" ]; "tls-ring" = [ "tonic/tls-ring" ]; - "tls-roots" = [ "tls" "tonic/tls-native-roots" ]; - "tls-webpki-roots" = [ "tls" "tonic/tls-webpki-roots" ]; + "tls-roots" = [ "tonic/tls-native-roots" ]; + "tls-webpki-roots" = [ "tonic/tls-webpki-roots" ]; "tokio" = [ "dep:tokio" ]; "tonic" = [ "dep:tonic" ]; + "tonic-types" = [ "dep:tonic-types" ]; "trace" = [ "opentelemetry/trace" "opentelemetry_sdk/trace" "opentelemetry-proto/trace" ]; - "tracing" = [ "dep:tracing" ]; "zstd" = [ "dep:zstd" ]; "zstd-http" = [ "zstd" ]; "zstd-tonic" = [ "tonic/zstd" ]; }; - resolvedDefaultFeatures = [ "default" "grpc-tonic" "gzip-tonic" "http" "http-proto" "internal-logs" "logs" "metrics" "opentelemetry-http" "prost" "reqwest" "reqwest-blocking-client" "tokio" "tonic" "trace" "tracing" ]; + resolvedDefaultFeatures = [ "default" "grpc-tonic" "gzip-tonic" "http" "http-proto" "internal-logs" "logs" "metrics" "opentelemetry-http" "prost" "reqwest" "reqwest-blocking-client" "tokio" "tonic" "tonic-types" "trace" ]; }; "opentelemetry-proto" = rec { crateName = "opentelemetry-proto"; - version = "0.31.0"; + version = "0.32.0"; edition = "2021"; - sha256 = "03xkjsjrsm7zkkx5gascqd9bg2z20wymm06l16cyxsp5dpq5s5x7"; + sha256 = "0f5ny4rpnpq6q5q34b8k2q548rf31rpbxkwjqjwzfqxg3yx5imjn"; libName = "opentelemetry_proto"; dependencies = [ { @@ -6373,30 +6323,29 @@ rec { "const-hex" = [ "dep:const-hex" ]; "default" = [ "full" ]; "full" = [ "gen-tonic" "trace" "logs" "metrics" "zpages" "with-serde" "internal-logs" ]; - "gen-tonic" = [ "gen-tonic-messages" "tonic/channel" ]; - "gen-tonic-messages" = [ "tonic" "tonic-prost" "prost" ]; + "gen-tonic" = [ "gen-tonic-messages" "tonic" "tonic-prost" "tonic/channel" ]; + "gen-tonic-messages" = [ "prost" ]; "internal-logs" = [ "opentelemetry/internal-logs" ]; "logs" = [ "opentelemetry/logs" "opentelemetry_sdk/logs" ]; "metrics" = [ "opentelemetry/metrics" "opentelemetry_sdk/metrics" ]; "prost" = [ "dep:prost" ]; "schemars" = [ "dep:schemars" ]; "serde" = [ "dep:serde" ]; - "serde_json" = [ "dep:serde_json" ]; "testing" = [ "opentelemetry/testing" ]; "tonic" = [ "dep:tonic" ]; "tonic-prost" = [ "dep:tonic-prost" ]; "trace" = [ "opentelemetry/trace" "opentelemetry_sdk/trace" ]; "with-schemars" = [ "schemars" ]; - "with-serde" = [ "serde" "const-hex" "base64" "serde_json" ]; + "with-serde" = [ "serde" "const-hex" "base64" ]; "zpages" = [ "trace" ]; }; resolvedDefaultFeatures = [ "gen-tonic" "gen-tonic-messages" "logs" "metrics" "prost" "tonic" "tonic-prost" "trace" ]; }; "opentelemetry-semantic-conventions" = rec { crateName = "opentelemetry-semantic-conventions"; - version = "0.31.0"; + version = "0.32.0"; edition = "2021"; - sha256 = "0in8plv2l2ar7anzi7lrbll0fjfvaymkg5vc5bnvibs1w3gjjbp6"; + sha256 = "0s1x4h1cgmhkxb7i5g02la2vhkf4lg5g26cgn2s2gd1p0j5gk8kc"; libName = "opentelemetry_semantic_conventions"; features = { }; @@ -6404,9 +6353,9 @@ rec { }; "opentelemetry_sdk" = rec { crateName = "opentelemetry_sdk"; - version = "0.31.0"; + version = "0.32.1"; edition = "2021"; - sha256 = "1gbjsggdxfpjbanjvaxa3nq32vfa37i3v13dvx4gsxhrk7sy8jp1"; + sha256 = "1ycl11syranrinhgn4c2hlzhyzyvpa06ryxq5mxgzmf4387ghncv"; dependencies = [ { name = "futures-channel"; @@ -6432,6 +6381,13 @@ rec { packageId = "percent-encoding"; optional = true; } + { + name = "portable-atomic"; + packageId = "portable-atomic"; + usesDefaultFeatures = false; + target = { target, features }: (!("64" == target."has_atomic" or null)); + features = [ "fallback" ]; + } { name = "rand"; packageId = "rand 0.9.4"; @@ -6456,10 +6412,18 @@ rec { optional = true; } ]; + devDependencies = [ + { + name = "tokio"; + packageId = "tokio"; + usesDefaultFeatures = false; + features = [ "macros" "rt-multi-thread" ]; + } + ]; features = { "default" = [ "trace" "metrics" "logs" "internal-logs" ]; "experimental_logs_batch_log_processor_with_async_runtime" = [ "logs" "experimental_async_runtime" ]; - "experimental_logs_concurrent_log_processor" = [ "logs" ]; + "experimental_metrics_bound_instruments" = [ "metrics" "opentelemetry/experimental_metrics_bound_instruments" ]; "experimental_metrics_custom_reader" = [ "metrics" ]; "experimental_metrics_disable_name_validation" = [ "metrics" ]; "experimental_metrics_periodicreader_with_async_runtime" = [ "metrics" "experimental_async_runtime" ]; @@ -6476,15 +6440,14 @@ rec { "rt-tokio-current-thread" = [ "tokio/rt" "tokio/time" "tokio-stream" "experimental_async_runtime" ]; "serde" = [ "dep:serde" ]; "serde_json" = [ "dep:serde_json" ]; - "spec_unstable_logs_enabled" = [ "logs" "opentelemetry/spec_unstable_logs_enabled" ]; "spec_unstable_metrics_views" = [ "metrics" ]; - "testing" = [ "opentelemetry/testing" "trace" "metrics" "logs" "rt-tokio" "rt-tokio-current-thread" "tokio/macros" "tokio/rt-multi-thread" ]; + "testing" = [ "opentelemetry/testing" "trace" "metrics" "logs" "tokio/sync" ]; "tokio" = [ "dep:tokio" ]; "tokio-stream" = [ "dep:tokio-stream" ]; "trace" = [ "opentelemetry/trace" "rand" "percent-encoding" ]; "url" = [ "dep:url" ]; }; - resolvedDefaultFeatures = [ "default" "experimental_async_runtime" "internal-logs" "logs" "metrics" "percent-encoding" "rand" "rt-tokio" "spec_unstable_logs_enabled" "tokio" "tokio-stream" "trace" ]; + resolvedDefaultFeatures = [ "default" "experimental_async_runtime" "internal-logs" "logs" "metrics" "percent-encoding" "rand" "rt-tokio" "tokio" "tokio-stream" "trace" ]; }; "ordered-float" = rec { crateName = "ordered-float"; @@ -6819,7 +6782,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; features = { @@ -6858,9 +6821,9 @@ rec { }; "pin-project" = rec { crateName = "pin-project"; - version = "1.1.11"; + version = "1.1.13"; edition = "2021"; - sha256 = "05zm3y3bl83ypsr6favxvny2kys4i19jiz1y18ylrbxwsiz9qx7i"; + sha256 = "09091qp946lpmjz4yp0xil1r5v4hgc91fi19dg5csayhdqrv4ri4"; libName = "pin_project"; dependencies = [ { @@ -6872,9 +6835,9 @@ rec { }; "pin-project-internal" = rec { crateName = "pin-project-internal"; - version = "1.1.11"; + version = "1.1.13"; edition = "2021"; - sha256 = "1ik4mpb92da75inmjvxf2qm61vrnwml3x24wddvrjlqh1z9hxcnr"; + sha256 = "12rzlh07i1sdgrvzj6wgkka5bjqyvbfsl8knq6qi7g16m7q9aqy9"; procMacro = true; libName = "pin_project_internal"; dependencies = [ @@ -6888,7 +6851,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; usesDefaultFeatures = false; features = [ "parsing" "printing" "clone-impls" "proc-macro" "full" "visit-mut" ]; } @@ -6993,7 +6956,7 @@ rec { "default" = [ "fallback" ]; "serde" = [ "dep:serde" ]; }; - resolvedDefaultFeatures = [ "require-cas" ]; + resolvedDefaultFeatures = [ "fallback" "require-cas" ]; }; "portable-atomic-util" = rec { crateName = "portable-atomic-util"; @@ -7196,9 +7159,9 @@ rec { }; "prost" = rec { crateName = "prost"; - version = "0.14.3"; + version = "0.14.4"; edition = "2021"; - sha256 = "0s057z9nzggzy7x4bbsiar852hg7zb81f4z4phcdb0ig99971snj"; + sha256 = "1qas5v5rap45f43v3ja0jngxrrafrkcwl0iw5a3ld1pz2rscd2jj"; authors = [ "Dan Burkert " "Lucio Franco " @@ -7225,9 +7188,9 @@ rec { }; "prost-derive" = rec { crateName = "prost-derive"; - version = "0.14.3"; + version = "0.14.4"; edition = "2021"; - sha256 = "02zvva6kb0pfvlyc4nac6gd37ncjrs8jq5scxcq4nbqkc8wh5ii7"; + sha256 = "1pqa77d7da5pf6ba3kjj7510m5cynz6902ax01ckvr0pfrgv4w5m"; procMacro = true; libName = "prost_derive"; authors = [ @@ -7255,12 +7218,40 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "extra-traits" ]; } ]; }; + "prost-types" = rec { + crateName = "prost-types"; + version = "0.14.4"; + edition = "2021"; + sha256 = "02ivjvc4cwl5bfgjs3l00hwlrk74z8zlg1xcgx60bww8fvf6fjgr"; + libName = "prost_types"; + authors = [ + "Dan Burkert " + "Lucio Franco " + "Casper Meijn " + "Tokio Contributors " + ]; + dependencies = [ + { + name = "prost"; + packageId = "prost"; + usesDefaultFeatures = false; + features = [ "derive" ]; + } + ]; + features = { + "arbitrary" = [ "dep:arbitrary" ]; + "chrono" = [ "dep:chrono" ]; + "default" = [ "std" ]; + "std" = [ "prost/std" ]; + }; + resolvedDefaultFeatures = [ "default" "std" ]; + }; "quote" = rec { crateName = "quote"; version = "1.0.45"; @@ -7533,16 +7524,16 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; }; "regex" = rec { crateName = "regex"; - version = "1.12.3"; + version = "1.12.4"; edition = "2021"; - sha256 = "0xp2q0x7ybmpa5zlgaz00p8zswcirj9h8nry3rxxsdwi9fhm81z1"; + sha256 = "1fm6si2xpmhwqflabdqsakc0qkq718wx2ljl37nbj75fb5vjnagi"; authors = [ "The Rust Project Developers" "Andrew Gallant " @@ -7659,9 +7650,9 @@ rec { }; "regex-syntax" = rec { crateName = "regex-syntax"; - version = "0.8.10"; + version = "0.8.11"; edition = "2021"; - sha256 = "02jx311ka0daxxc7v45ikzhcl3iydjbbb0mdrpc1xgg8v7c7v2fw"; + sha256 = "1m25h5q2wp976fb9gc3dsc9l99svcvd5cri8lncb51c46ydgzxnn"; libName = "regex_syntax"; authors = [ "The Rust Project Developers" @@ -7690,9 +7681,9 @@ rec { }; "reqwest" = rec { crateName = "reqwest"; - version = "0.12.28"; + version = "0.13.4"; edition = "2021"; - sha256 = "0iqidijghgqbzl3bjg5hb4zmigwa4r612bgi0yiq0c90b6jkrpgd"; + sha256 = "1hy1plns9krbh3h1dy2sdjygsfkdcnxm6pbxdi0ya9b5vq8mi711"; authors = [ "Sean McArthur " ]; @@ -7709,7 +7700,7 @@ rec { name = "futures-channel"; packageId = "futures-channel"; optional = true; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); } { name = "futures-core"; @@ -7729,62 +7720,44 @@ rec { { name = "http-body"; packageId = "http-body"; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); } { name = "http-body-util"; packageId = "http-body-util"; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); } { name = "hyper"; packageId = "hyper"; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); features = [ "http1" "client" ]; } { name = "hyper-util"; packageId = "hyper-util"; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); features = [ "http1" "client" "client-legacy" "client-proxy" "tokio" ]; } { name = "js-sys"; packageId = "js-sys"; - target = { target, features }: ("wasm32" == target."arch" or null); + target = { target, features }: (("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null))); } { name = "log"; packageId = "log"; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); } { name = "percent-encoding"; packageId = "percent-encoding"; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); } { name = "pin-project-lite"; packageId = "pin-project-lite"; - target = { target, features }: (!("wasm32" == target."arch" or null)); - } - { - name = "serde"; - packageId = "serde"; - } - { - name = "serde_json"; - packageId = "serde_json"; - optional = true; - } - { - name = "serde_json"; - packageId = "serde_json"; - target = { target, features }: ("wasm32" == target."arch" or null); - } - { - name = "serde_urlencoded"; - packageId = "serde_urlencoded"; + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); } { name = "sync_wrapper"; @@ -7795,27 +7768,27 @@ rec { name = "tokio"; packageId = "tokio"; usesDefaultFeatures = false; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); features = [ "net" "time" ]; } { name = "tower"; packageId = "tower"; usesDefaultFeatures = false; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); features = [ "retry" "timeout" "util" ]; } { name = "tower-http"; packageId = "tower-http"; usesDefaultFeatures = false; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); features = [ "follow-redirect" ]; } { name = "tower-service"; packageId = "tower-service"; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); } { name = "url"; @@ -7824,17 +7797,17 @@ rec { { name = "wasm-bindgen"; packageId = "wasm-bindgen"; - target = { target, features }: ("wasm32" == target."arch" or null); + target = { target, features }: (("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null))); } { name = "wasm-bindgen-futures"; packageId = "wasm-bindgen-futures"; - target = { target, features }: ("wasm32" == target."arch" or null); + target = { target, features }: (("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null))); } { name = "web-sys"; packageId = "web-sys"; - target = { target, features }: ("wasm32" == target."arch" or null); + target = { target, features }: (("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null))); features = [ "AbortController" "AbortSignal" "Headers" "Request" "RequestInit" "RequestMode" "Response" "Window" "FormData" "Blob" "BlobPropertyBag" "ServiceWorkerGlobalScope" "RequestCredentials" "File" "ReadableStream" "RequestCache" ]; } ]; @@ -7843,33 +7816,27 @@ rec { name = "futures-util"; packageId = "futures-util"; usesDefaultFeatures = false; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); features = [ "std" "alloc" ]; } { name = "hyper"; packageId = "hyper"; usesDefaultFeatures = false; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); features = [ "http1" "http2" "client" "server" ]; } { name = "hyper-util"; packageId = "hyper-util"; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); features = [ "http1" "http2" "client" "client-legacy" "server-auto" "server-graceful" "tokio" ]; } - { - name = "serde"; - packageId = "serde"; - target = { target, features }: (!("wasm32" == target."arch" or null)); - features = [ "derive" ]; - } { name = "tokio"; packageId = "tokio"; usesDefaultFeatures = false; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); features = [ "macros" "rt-multi-thread" ]; } { @@ -7881,40 +7848,37 @@ rec { { name = "wasm-bindgen"; packageId = "wasm-bindgen"; - target = { target, features }: ("wasm32" == target."arch" or null); + target = { target, features }: (("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null))); features = [ "serde-serialize" ]; } ]; features = { + "__native-tls" = [ "dep:hyper-tls" "dep:native-tls-crate" "__tls" "dep:tokio-native-tls" ]; + "__native-tls-alpn" = [ "native-tls-crate?/alpn" "hyper-tls?/alpn" ]; "__rustls" = [ "dep:hyper-rustls" "dep:tokio-rustls" "dep:rustls" "__tls" ]; - "__rustls-ring" = [ "hyper-rustls?/ring" "tokio-rustls?/ring" "rustls?/ring" "quinn?/ring" ]; + "__rustls-aws-lc-rs" = [ "hyper-rustls?/aws-lc-rs" "tokio-rustls?/aws-lc-rs" "rustls?/aws-lc-rs" "quinn?/rustls-aws-lc-rs" ]; "__tls" = [ "dep:rustls-pki-types" "tokio/io-util" ]; "blocking" = [ "dep:futures-channel" "futures-channel?/sink" "dep:futures-util" "futures-util?/io" "futures-util?/sink" "tokio/sync" ]; "brotli" = [ "tower-http/decompression-br" ]; "charset" = [ "dep:encoding_rs" "dep:mime" ]; "cookies" = [ "dep:cookie_crate" "dep:cookie_store" ]; "default" = [ "default-tls" "charset" "http2" "system-proxy" ]; - "default-tls" = [ "dep:hyper-tls" "dep:native-tls-crate" "__tls" "dep:tokio-native-tls" ]; + "default-tls" = [ "rustls" ]; "deflate" = [ "tower-http/decompression-deflate" ]; + "form" = [ "dep:serde" "dep:serde_urlencoded" ]; "gzip" = [ "tower-http/decompression-gzip" ]; - "h2" = [ "dep:h2" ]; "hickory-dns" = [ "dep:hickory-resolver" "dep:once_cell" ]; - "http2" = [ "h2" "hyper/http2" "hyper-util/http2" "hyper-rustls?/http2" ]; - "http3" = [ "rustls-tls-manual-roots" "dep:h3" "dep:h3-quinn" "dep:quinn" "tokio/macros" ]; - "json" = [ "dep:serde_json" ]; - "macos-system-configuration" = [ "system-proxy" ]; + "http2" = [ "dep:h2" "hyper/http2" "hyper-util/http2" "hyper-rustls?/http2" ]; + "http3" = [ "rustls" "dep:h3" "dep:h3-quinn" "dep:quinn" "tokio/macros" ]; + "json" = [ "dep:serde" "dep:serde_json" ]; "multipart" = [ "dep:mime_guess" "dep:futures-util" ]; - "native-tls" = [ "default-tls" ]; - "native-tls-alpn" = [ "native-tls" "native-tls-crate?/alpn" "hyper-tls?/alpn" ]; - "native-tls-vendored" = [ "native-tls" "native-tls-crate?/vendored" ]; - "rustls-tls" = [ "rustls-tls-webpki-roots" ]; - "rustls-tls-manual-roots" = [ "rustls-tls-manual-roots-no-provider" "__rustls-ring" ]; - "rustls-tls-manual-roots-no-provider" = [ "__rustls" ]; - "rustls-tls-native-roots" = [ "rustls-tls-native-roots-no-provider" "__rustls-ring" ]; - "rustls-tls-native-roots-no-provider" = [ "dep:rustls-native-certs" "hyper-rustls?/native-tokio" "__rustls" ]; - "rustls-tls-no-provider" = [ "rustls-tls-manual-roots-no-provider" ]; - "rustls-tls-webpki-roots" = [ "rustls-tls-webpki-roots-no-provider" "__rustls-ring" ]; - "rustls-tls-webpki-roots-no-provider" = [ "dep:webpki-roots" "hyper-rustls?/webpki-tokio" "__rustls" ]; + "native-tls" = [ "__native-tls" "__native-tls-alpn" ]; + "native-tls-no-alpn" = [ "__native-tls" ]; + "native-tls-vendored" = [ "__native-tls" "native-tls-crate?/vendored" "__native-tls-alpn" ]; + "native-tls-vendored-no-alpn" = [ "__native-tls" "native-tls-crate?/vendored" ]; + "query" = [ "dep:serde" "dep:serde_urlencoded" ]; + "rustls" = [ "__rustls-aws-lc-rs" "dep:rustls-platform-verifier" "__rustls" ]; + "rustls-no-provider" = [ "dep:rustls-platform-verifier" "__rustls" ]; "stream" = [ "tokio/fs" "dep:futures-util" "dep:tokio-util" "dep:wasm-streams" ]; "system-proxy" = [ "hyper-util/client-proxy-system" ]; "zstd" = [ "tower-http/decompression-zstd" ]; @@ -8191,7 +8155,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" "parsing" "extra-traits" "visit" "visit-mut" ]; } { @@ -8226,9 +8190,9 @@ rec { }; "rustls" = rec { crateName = "rustls"; - version = "0.23.39"; + version = "0.23.40"; edition = "2021"; - sha256 = "03p6fkdwbdpp93dfidc4nzgmalwp3gxnv0rk421a5k3pn2612b3w"; + sha256 = "12qnv3ag4wrw7aj8jng74kgrilpjm2b1rfcjaac8h691frccv1pg"; dependencies = [ { name = "log"; @@ -8295,9 +8259,9 @@ rec { }; "rustls-native-certs" = rec { crateName = "rustls-native-certs"; - version = "0.8.3"; + version = "0.8.4"; edition = "2021"; - sha256 = "0qrajg2n90bcr3bcq6j95gjm7a9lirfkkdmjj32419dyyzan0931"; + sha256 = "0kgazl8zc1sv63qg179bz96ilzh56lzfa5k92ji7d265f4kibdfs"; libName = "rustls_native_certs"; dependencies = [ { @@ -8533,13 +8497,13 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; devDependencies = [ { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "extra-traits" ]; } ]; @@ -8823,7 +8787,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; usesDefaultFeatures = false; features = [ "clone-impls" "derive" "parsing" "printing" "proc-macro" ]; } @@ -8855,7 +8819,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; usesDefaultFeatures = false; features = [ "clone-impls" "derive" "parsing" "printing" ]; } @@ -8864,9 +8828,9 @@ rec { }; "serde_json" = rec { crateName = "serde_json"; - version = "1.0.149"; + version = "1.0.150"; edition = "2021"; - sha256 = "11jdx4vilzrjjd1dpgy67x5lgzr0laplz30dhv75lnf5ffa07z43"; + sha256 = "1ffgfhy9kndjnrz8lmy95pr758p2zk8dxv6yi99x0vkkni24w0g8"; authors = [ "Erick Tryzelaar " "David Tolnay " @@ -9107,9 +9071,9 @@ rec { }; "shlex" = rec { crateName = "shlex"; - version = "1.3.0"; - edition = "2015"; - sha256 = "0r1y6bv26c1scpxvhg2cabimrmwgbp4p3wy6syj9n0c4s3q2znhg"; + version = "2.0.1"; + edition = "2018"; + sha256 = "1fjsll1cd7d2bcpdij9kd6w62rpbc7qqzvydvs021vsmr1cxvypq"; authors = [ "comex " "Fenhl " @@ -9204,9 +9168,9 @@ rec { }; "smallvec" = rec { crateName = "smallvec"; - version = "1.15.1"; + version = "1.15.2"; edition = "2018"; - sha256 = "00xxdxxpgyq5vjnpljvkmy99xij5rxgh913ii1v16kzynnivgcb7"; + sha256 = "143wzbqf6vgapdp2z4qpl0yvlqcn17s8cnk8m28rqly808zsdmlf"; authors = [ "The Servo Project Developers" ]; @@ -9288,29 +9252,25 @@ rec { }; resolvedDefaultFeatures = [ "alloc" "default" "rust_1_61" "rust_1_65" "std" ]; }; - "snafu 0.9.0" = rec { + "snafu 0.9.1" = rec { crateName = "snafu"; - version = "0.9.0"; + version = "0.9.1"; edition = "2018"; - sha256 = "1ii9r99x5qcn754m624yzgb9hzvkqkrcygf0aqh0pyb9dbnvrm6i"; + sha256 = "08k5yfydxdlshivfhrdq9km8qn02r93q28gkyvazbqz2icr1586i"; authors = [ "Jake Goulding " ]; dependencies = [ { name = "snafu-derive"; - packageId = "snafu-derive 0.9.0"; + packageId = "snafu-derive 0.9.1"; } ]; features = { - "backtrace" = [ "dep:backtrace" ]; - "backtraces-impl-backtrace-crate" = [ "backtrace" ]; + "backtraces-impl-backtrace-crate" = [ "dep:backtrace" ]; "default" = [ "std" "rust_1_81" ]; - "futures" = [ "futures-core-crate" "pin-project" ]; - "futures-core-crate" = [ "dep:futures-core-crate" ]; - "futures-crate" = [ "dep:futures-crate" ]; - "internal-dev-dependencies" = [ "futures-crate" ]; - "pin-project" = [ "dep:pin-project" ]; + "futures" = [ "dep:futures-core" "dep:pin-project" ]; + "internal-dev-dependencies" = [ "dep:futures" ]; "std" = [ "alloc" ]; "unstable-provider-api" = [ "snafu-derive/unstable-provider-api" ]; }; @@ -9370,7 +9330,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" ]; } ]; @@ -9378,11 +9338,11 @@ rec { }; resolvedDefaultFeatures = [ "rust_1_61" ]; }; - "snafu-derive 0.9.0" = rec { + "snafu-derive 0.9.1" = rec { crateName = "snafu-derive"; - version = "0.9.0"; + version = "0.9.1"; edition = "2018"; - sha256 = "0h0x61kyj4fvilcr2nj02l85shw1ika64vq9brf2gyna662ln9al"; + sha256 = "1nkfi7bis72pz3w7vb64m79w49qsv20sbf19jkd471vbhr83q42z"; procMacro = true; libName = "snafu_derive"; authors = [ @@ -9406,9 +9366,9 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; usesDefaultFeatures = false; - features = [ "clone-impls" "derive" "full" "parsing" "printing" "proc-macro" ]; + features = [ "clone-impls" "derive" "full" "parsing" "printing" "proc-macro" "visit-mut" ]; } ]; features = { @@ -9416,9 +9376,9 @@ rec { }; "socket2" = rec { crateName = "socket2"; - version = "0.6.3"; + version = "0.6.4"; edition = "2021"; - sha256 = "0gkjjcyn69hqhhlh5kl8byk5m0d7hyrp2aqwzbs3d33q208nwxis"; + sha256 = "0ldyp5rhba15spwxj1n94xh7sjks1398c3vwpwkxkd1087nwzlaj"; authors = [ "Alex Crichton " "Thomas de Zeeuw " @@ -9516,9 +9476,9 @@ rec { edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; libName = "stackable_certs"; authors = [ @@ -9576,7 +9536,7 @@ rec { } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } { name = "stackable-shared"; @@ -9649,10 +9609,6 @@ rec { name = "indoc"; packageId = "indoc"; } - { - name = "product-config"; - packageId = "product-config"; - } { name = "serde"; packageId = "serde"; @@ -9664,7 +9620,7 @@ rec { } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } { name = "stackable-operator"; @@ -9711,9 +9667,9 @@ rec { edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; libName = "stackable_operator"; authors = [ @@ -9763,6 +9719,10 @@ rec { name = "indexmap"; packageId = "indexmap"; } + { + name = "java-properties"; + packageId = "java-properties"; + } { name = "jiff"; packageId = "jiff"; @@ -9770,6 +9730,7 @@ rec { { name = "json-patch"; packageId = "json-patch"; + features = [ "schemars" ]; } { name = "k8s-openapi"; @@ -9817,9 +9778,14 @@ rec { name = "serde_yaml"; packageId = "serde_yaml"; } + { + name = "sha2"; + packageId = "sha2"; + features = [ "oid" ]; + } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } { name = "stackable-operator-derive"; @@ -9873,12 +9839,21 @@ rec { packageId = "url"; features = [ "serde" ]; } + { + name = "uuid"; + packageId = "uuid"; + } + { + name = "xml"; + packageId = "xml"; + } ]; features = { "certs" = [ "dep:stackable-certs" ]; + "client-feature-gates" = [ "dep:winnow" ]; "crds" = [ "dep:stackable-versioned" ]; "default" = [ "crds" ]; - "full" = [ "crds" "certs" "time" "webhook" "kube-ws" ]; + "full" = [ "client-feature-gates" "crds" "certs" "time" "webhook" "kube-ws" ]; "kube-ws" = [ "kube/ws" ]; "time" = [ "stackable-shared/time" ]; "webhook" = [ "dep:stackable-webhook" ]; @@ -9891,9 +9866,9 @@ rec { edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; procMacro = true; libName = "stackable_operator_derive"; @@ -9915,20 +9890,20 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; }; "stackable-shared" = rec { crateName = "stackable-shared"; - version = "0.1.0"; + version = "0.1.1"; edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; libName = "stackable_shared"; authors = [ @@ -9972,7 +9947,7 @@ rec { } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } { name = "strum"; @@ -10003,13 +9978,13 @@ rec { }; "stackable-telemetry" = rec { crateName = "stackable-telemetry"; - version = "0.6.3"; + version = "0.6.4"; edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; libName = "stackable_telemetry"; authors = [ @@ -10052,7 +10027,7 @@ rec { { name = "opentelemetry_sdk"; packageId = "opentelemetry_sdk"; - features = [ "rt-tokio" "logs" "rt-tokio" "spec_unstable_logs_enabled" ]; + features = [ "rt-tokio" "logs" "rt-tokio" ]; } { name = "pin-project"; @@ -10060,7 +10035,7 @@ rec { } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } { name = "strum"; @@ -10117,9 +10092,9 @@ rec { edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; libName = "stackable_versioned"; authors = [ @@ -10152,7 +10127,7 @@ rec { } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } { name = "stackable-versioned-macros"; @@ -10167,9 +10142,9 @@ rec { edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; procMacro = true; libName = "stackable_versioned_macros"; @@ -10224,7 +10199,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; @@ -10235,9 +10210,9 @@ rec { edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; libName = "stackable_webhook"; authors = [ @@ -10309,7 +10284,7 @@ rec { } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } { name = "stackable-certs"; @@ -10419,7 +10394,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "parsing" ]; } ]; @@ -10483,11 +10458,11 @@ rec { }; resolvedDefaultFeatures = [ "clone-impls" "default" "derive" "full" "parsing" "printing" "proc-macro" "quote" ]; }; - "syn 2.0.117" = rec { + "syn 2.0.118" = rec { crateName = "syn"; - version = "2.0.117"; + version = "2.0.118"; edition = "2021"; - sha256 = "16cv7c0wbn8amxc54n4w15kxlx5ypdmla8s0gxr2l7bv7s0bhrg6"; + sha256 = "08hlbc32lqd5d67p26ck7chg0rkclsw9as6f96vfn4s2j1zyb6hv"; authors = [ "David Tolnay " ]; @@ -10559,7 +10534,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; usesDefaultFeatures = false; features = [ "derive" "parsing" "printing" "clone-impls" "visit" "extra-traits" ]; } @@ -10626,7 +10601,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; @@ -10652,7 +10627,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; @@ -10676,9 +10651,9 @@ rec { }; "time" = rec { crateName = "time"; - version = "0.3.47"; + version = "0.3.49"; edition = "2024"; - sha256 = "0b7g9ly2iabrlgizliz6v5x23yq5d6bpp0mqz6407z1s526d8fvl"; + sha256 = "0sc4dgw6g187gvz5qj9iqqk2ashqzvdwi664b2183gbvsk1566ki"; authors = [ "Jacob Pratt " "Time contributors" @@ -10687,12 +10662,6 @@ rec { { name = "deranged"; packageId = "deranged"; - features = [ "powerfmt" ]; - } - { - name = "itoa"; - packageId = "itoa"; - optional = true; } { name = "num-conv"; @@ -10732,13 +10701,14 @@ rec { features = { "alloc" = [ "serde_core?/alloc" ]; "default" = [ "std" ]; - "formatting" = [ "dep:itoa" "std" "time-macros?/formatting" ]; + "formatting" = [ "std" "time-macros?/formatting" ]; "large-dates" = [ "time-core/large-dates" "time-macros?/large-dates" ]; "local-offset" = [ "std" "dep:libc" "dep:num_threads" ]; "macros" = [ "dep:time-macros" ]; "parsing" = [ "time-macros?/parsing" ]; "quickcheck" = [ "dep:quickcheck" "alloc" "deranged/quickcheck" ]; - "rand" = [ "rand08" "rand09" ]; + "rand" = [ "rand08" "rand09" "rand010" ]; + "rand010" = [ "dep:rand010" "deranged/rand010" ]; "rand08" = [ "dep:rand08" "deranged/rand08" ]; "rand09" = [ "dep:rand09" "deranged/rand09" ]; "serde" = [ "dep:serde_core" "time-macros?/serde" "deranged/serde" ]; @@ -10751,9 +10721,9 @@ rec { }; "time-core" = rec { crateName = "time-core"; - version = "0.1.8"; + version = "0.1.9"; edition = "2024"; - sha256 = "1jidl426mw48i7hjj4hs9vxgd9lwqq4vyalm4q8d7y4iwz7y353n"; + sha256 = "028ix0ax7ixp1h1k5zsqwgw85w6y1q32irslma7ci6ddd5kr074y"; libName = "time_core"; authors = [ "Jacob Pratt " @@ -10764,9 +10734,9 @@ rec { }; "time-macros" = rec { crateName = "time-macros"; - version = "0.2.27"; + version = "0.2.29"; edition = "2024"; - sha256 = "058ja265waq275wxvnfwavbz9r1hd4dgwpfn7a1a9a70l32y8w1f"; + sha256 = "0zf1ycfikg93ijf00qnprk801khqnqqga1zp0adbp73sfaim5iki"; procMacro = true; libName = "time_macros"; authors = [ @@ -10869,7 +10839,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "parsing" ]; } ]; @@ -10881,9 +10851,9 @@ rec { }; "tokio" = rec { crateName = "tokio"; - version = "1.52.1"; + version = "1.52.3"; edition = "2021"; - sha256 = "1imw1dkkv38p66i33m5hsyk3d6prsbyrayjvqhndjvz89ybywzdn"; + sha256 = "1zpzazypkg61sw91na1m85x5s4rsjym335fwwhwm1hcs70dz1iwg"; authors = [ "Tokio Contributors " ]; @@ -11021,7 +10991,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" ]; } ]; @@ -11193,9 +11163,9 @@ rec { }; "toml_edit" = rec { crateName = "toml_edit"; - version = "0.25.11+spec-1.1.0"; + version = "0.25.12+spec-1.1.0"; edition = "2024"; - sha256 = "0awzffbkx33v9x4h19b5mfrwp3sn4ifr16y58sbk6j6l5v9c8n8b"; + sha256 = "1mx5paq837rjw7w51zprrjynk1vaig9yzxfqz9ac79jmd7f3w5fj"; dependencies = [ { name = "indexmap"; @@ -11248,9 +11218,9 @@ rec { }; "tonic" = rec { crateName = "tonic"; - version = "0.14.5"; - edition = "2021"; - sha256 = "1v4k7aa28m7722gz9qak2jiy7lis1ycm4fdmq63iip4m0qdcdizy"; + version = "0.14.6"; + edition = "2024"; + sha256 = "1vs5ci6z6b9xhfsnx4s8qx6bqi1zzcrxncjp71147a0gqwc5aamc"; authors = [ "Lucio Franco " ]; @@ -11377,9 +11347,9 @@ rec { }; "tonic-prost" = rec { crateName = "tonic-prost"; - version = "0.14.5"; - edition = "2021"; - sha256 = "02fkg2bv87q0yds2wz3w0s7i1x6qcgbrl00dy6ipajdapfh7clx5"; + version = "0.14.6"; + edition = "2024"; + sha256 = "184y40nf0iyzc5rg32ivgd88snv68sqy1kchynn55r1vhml9z12h"; libName = "tonic_prost"; authors = [ "Lucio Franco " @@ -11400,6 +11370,33 @@ rec { } ]; + }; + "tonic-types" = rec { + crateName = "tonic-types"; + version = "0.14.6"; + edition = "2024"; + sha256 = "1s286gg71pjajny8xar0azq1w9lgz1ks3jm3pccxb0qz0q11pavk"; + libName = "tonic_types"; + authors = [ + "Lucio Franco " + "Rafael Lemos " + ]; + dependencies = [ + { + name = "prost"; + packageId = "prost"; + } + { + name = "prost-types"; + packageId = "prost-types"; + } + { + name = "tonic"; + packageId = "tonic"; + usesDefaultFeatures = false; + } + ]; + }; "tower" = rec { crateName = "tower"; @@ -11521,9 +11518,9 @@ rec { }; "tower-http" = rec { crateName = "tower-http"; - version = "0.6.8"; + version = "0.6.11"; edition = "2018"; - sha256 = "1y514jwzbyrmrkbaajpwmss4rg0mak82k16d6588w9ncaffmbrnl"; + sha256 = "0h08wjgs3hwnq11iwwzlmnabn1h4cl0fzd48svaccvqffkiggz2c"; libName = "tower_http"; authors = [ "Tower Maintainers " @@ -11557,11 +11554,6 @@ rec { packageId = "http-body"; optional = true; } - { - name = "iri-string"; - packageId = "iri-string"; - optional = true; - } { name = "mime"; packageId = "mime"; @@ -11591,6 +11583,11 @@ rec { optional = true; usesDefaultFeatures = false; } + { + name = "url"; + packageId = "url"; + optional = true; + } ]; devDependencies = [ { @@ -11612,35 +11609,33 @@ rec { } ]; features = { - "async-compression" = [ "dep:async-compression" ]; "auth" = [ "base64" "validate-request" ]; "base64" = [ "dep:base64" ]; "catch-panic" = [ "tracing" "futures-util/std" "dep:http-body" "dep:http-body-util" ]; - "compression-br" = [ "async-compression/brotli" "futures-core" "dep:http-body" "tokio-util" "tokio" ]; - "compression-deflate" = [ "async-compression/zlib" "futures-core" "dep:http-body" "tokio-util" "tokio" ]; + "compression-br" = [ "dep:async-compression" "async-compression?/brotli" "futures-core" "dep:http-body" "tokio-util" "dep:tokio" ]; + "compression-deflate" = [ "dep:async-compression" "async-compression?/zlib" "futures-core" "dep:http-body" "tokio-util" "dep:tokio" ]; "compression-full" = [ "compression-br" "compression-deflate" "compression-gzip" "compression-zstd" ]; - "compression-gzip" = [ "async-compression/gzip" "futures-core" "dep:http-body" "tokio-util" "tokio" ]; - "compression-zstd" = [ "async-compression/zstd" "futures-core" "dep:http-body" "tokio-util" "tokio" ]; - "decompression-br" = [ "async-compression/brotli" "futures-core" "dep:http-body" "dep:http-body-util" "tokio-util" "tokio" ]; - "decompression-deflate" = [ "async-compression/zlib" "futures-core" "dep:http-body" "dep:http-body-util" "tokio-util" "tokio" ]; + "compression-gzip" = [ "dep:async-compression" "async-compression?/gzip" "futures-core" "dep:http-body" "tokio-util" "dep:tokio" ]; + "compression-zstd" = [ "dep:async-compression" "async-compression?/zstd" "futures-core" "dep:http-body" "tokio-util" "dep:tokio" ]; + "decompression-br" = [ "dep:async-compression" "async-compression?/brotli" "futures-core" "dep:http-body" "dep:http-body-util" "tokio-util" "dep:tokio" ]; + "decompression-deflate" = [ "dep:async-compression" "async-compression?/zlib" "futures-core" "dep:http-body" "dep:http-body-util" "tokio-util" "dep:tokio" ]; "decompression-full" = [ "decompression-br" "decompression-deflate" "decompression-gzip" "decompression-zstd" ]; - "decompression-gzip" = [ "async-compression/gzip" "futures-core" "dep:http-body" "dep:http-body-util" "tokio-util" "tokio" ]; - "decompression-zstd" = [ "async-compression/zstd" "futures-core" "dep:http-body" "dep:http-body-util" "tokio-util" "tokio" ]; - "follow-redirect" = [ "futures-util" "dep:http-body" "iri-string" "tower/util" ]; - "fs" = [ "futures-core" "futures-util" "dep:http-body" "dep:http-body-util" "tokio/fs" "tokio-util/io" "tokio/io-util" "dep:http-range-header" "mime_guess" "mime" "percent-encoding" "httpdate" "set-status" "futures-util/alloc" "tracing" ]; - "full" = [ "add-extension" "auth" "catch-panic" "compression-full" "cors" "decompression-full" "follow-redirect" "fs" "limit" "map-request-body" "map-response-body" "metrics" "normalize-path" "propagate-header" "redirect" "request-id" "sensitive-headers" "set-header" "set-status" "timeout" "trace" "util" "validate-request" ]; + "decompression-gzip" = [ "dep:async-compression" "async-compression?/gzip" "futures-core" "dep:http-body" "dep:http-body-util" "tokio-util" "dep:tokio" ]; + "decompression-zstd" = [ "dep:async-compression" "async-compression?/zstd" "futures-core" "dep:http-body" "dep:http-body-util" "tokio-util" "dep:tokio" ]; + "follow-redirect" = [ "futures-util" "dep:http-body" "dep:url" "tower/util" ]; + "fs" = [ "dep:tokio" "tokio?/fs" "tokio?/io-util" "futures-core" "futures-util" "dep:http-body" "dep:http-body-util" "tokio-util/io" "dep:http-range-header" "mime_guess" "mime" "percent-encoding" "httpdate" "set-status" "futures-util/alloc" ]; + "full" = [ "add-extension" "auth" "catch-panic" "compression-full" "cors" "decompression-full" "follow-redirect" "fs" "limit" "map-request-body" "map-response-body" "metrics" "normalize-path" "on-early-drop" "propagate-header" "redirect" "request-id" "sensitive-headers" "set-header" "set-status" "timeout" "trace" "util" "validate-request" ]; "futures-core" = [ "dep:futures-core" ]; "futures-util" = [ "dep:futures-util" ]; "httpdate" = [ "dep:httpdate" ]; - "iri-string" = [ "dep:iri-string" ]; "limit" = [ "dep:http-body" "dep:http-body-util" ]; - "metrics" = [ "dep:http-body" "tokio/time" ]; + "metrics" = [ "dep:http-body" "dep:tokio" "tokio?/time" ]; "mime" = [ "dep:mime" ]; "mime_guess" = [ "dep:mime_guess" ]; + "on-early-drop" = [ "dep:http-body" ]; "percent-encoding" = [ "dep:percent-encoding" ]; "request-id" = [ "uuid" ]; - "timeout" = [ "dep:http-body" "tokio/time" ]; - "tokio" = [ "dep:tokio" ]; + "timeout" = [ "dep:http-body" "dep:tokio" "tokio?/time" ]; "tokio-util" = [ "dep:tokio-util" ]; "tower" = [ "dep:tower" ]; "trace" = [ "dep:http-body" "tracing" ]; @@ -11649,7 +11644,7 @@ rec { "uuid" = [ "dep:uuid" ]; "validate-request" = [ "mime" ]; }; - resolvedDefaultFeatures = [ "auth" "base64" "default" "follow-redirect" "futures-util" "iri-string" "map-response-body" "mime" "tower" "trace" "tracing" "util" "validate-request" ]; + resolvedDefaultFeatures = [ "auth" "base64" "default" "follow-redirect" "futures-util" "map-response-body" "mime" "tower" "trace" "tracing" "util" "validate-request" ]; }; "tower-layer" = rec { crateName = "tower-layer"; @@ -11783,7 +11778,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; usesDefaultFeatures = false; features = [ "full" "parsing" "printing" "visit-mut" "clone-impls" "extra-traits" "proc-macro" ]; } @@ -11856,9 +11851,9 @@ rec { }; "tracing-opentelemetry" = rec { crateName = "tracing-opentelemetry"; - version = "0.32.1"; + version = "0.33.0"; edition = "2021"; - sha256 = "1z2jjmxbkm1qawlb3bm99x8xwf4g8wjkbcknm9z4fv1w14nqzhhs"; + sha256 = "09nvxy5m7nxmifz4b6szdcyczapp2jcgxcac0jw4ax8klz5n9g5d"; libName = "tracing_opentelemetry"; dependencies = [ { @@ -12093,13 +12088,9 @@ rec { }; "typenum" = rec { crateName = "typenum"; - version = "1.20.0"; + version = "1.20.1"; edition = "2018"; - sha256 = "1pj35y6q11d3y55gdl6g1h2dfhmybjming0jdi9bh0bpnqm11kj0"; - authors = [ - "Paho Lurie-Gregg " - "Andre Bogus " - ]; + sha256 = "086s9ly0906kw5yw41249fba97w5zfxf03pyfwdkffvcprqfixdn"; features = { "scale-info" = [ "dep:scale-info" ]; "scale_info" = [ "scale-info/derive" ]; @@ -12132,9 +12123,9 @@ rec { }; "unicode-segmentation" = rec { crateName = "unicode-segmentation"; - version = "1.13.2"; + version = "1.13.3"; edition = "2018"; - sha256 = "135a26m4a0wj319gcw28j6a5aqvz00jmgwgmcs6szgxjf942facn"; + sha256 = "1a47zaq83p386r3baq4m018xd5q4q0grdg56i1x042dzn71x7xf6"; libName = "unicode_segmentation"; authors = [ "kwantam " @@ -12260,6 +12251,66 @@ rec { }; resolvedDefaultFeatures = [ "default" ]; }; + "uuid" = rec { + crateName = "uuid"; + version = "1.23.3"; + edition = "2021"; + sha256 = "1drddl03gi12vl1s3l2h371dw39plhn9wappp00v707g7h96nk8l"; + authors = [ + "Ashley Mannix" + "Dylan DPC" + "Hunar Roop Kahlon" + ]; + dependencies = [ + { + name = "js-sys"; + packageId = "js-sys"; + optional = true; + usesDefaultFeatures = false; + target = { target, features }: (("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)) && (builtins.elem "atomics" targetFeatures)); + } + { + name = "wasm-bindgen"; + packageId = "wasm-bindgen"; + optional = true; + usesDefaultFeatures = false; + target = { target, features }: (("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null))); + } + ]; + devDependencies = [ + { + name = "wasm-bindgen"; + packageId = "wasm-bindgen"; + target = { target, features }: (("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null))); + } + ]; + features = { + "arbitrary" = [ "dep:arbitrary" ]; + "atomic" = [ "dep:atomic" ]; + "borsh" = [ "dep:borsh" "dep:borsh-derive" ]; + "bytemuck" = [ "dep:bytemuck" ]; + "default" = [ "std" ]; + "fast-rng" = [ "rng" "dep:rand" ]; + "js" = [ "dep:wasm-bindgen" "dep:js-sys" ]; + "md5" = [ "dep:md-5" ]; + "rng" = [ "dep:getrandom" ]; + "rng-getrandom" = [ "rng" "dep:getrandom" "uuid-rng-internal-lib" "uuid-rng-internal-lib/getrandom" ]; + "rng-rand" = [ "rng" "dep:rand" "uuid-rng-internal-lib" "uuid-rng-internal-lib/rand" ]; + "serde" = [ "dep:serde_core" ]; + "sha1" = [ "dep:sha1_smol" ]; + "slog" = [ "dep:slog" ]; + "std" = [ "wasm-bindgen?/std" "js-sys?/std" ]; + "uuid-rng-internal-lib" = [ "dep:uuid-rng-internal-lib" ]; + "v1" = [ "atomic" ]; + "v3" = [ "md5" ]; + "v4" = [ "rng" ]; + "v5" = [ "sha1" ]; + "v6" = [ "atomic" ]; + "v7" = [ "rng" ]; + "zerocopy" = [ "dep:zerocopy" ]; + }; + resolvedDefaultFeatures = [ "default" "std" ]; + }; "valuable" = rec { crateName = "valuable"; version = "0.1.1"; @@ -12327,9 +12378,9 @@ rec { }; "wasip2" = rec { crateName = "wasip2"; - version = "1.0.3+wasi-0.2.9"; + version = "1.0.4+wasi-0.2.12"; edition = "2021"; - sha256 = "1mi3w855dz99xzjqc4aa8c9q5b6z1y5c963pkk4cvmr6vdr4c1i0"; + sha256 = "11wl7lqwq4pbmlmzr6n7bwz0hzy1z6sxc4554bkmrr86w4vznzmn"; dependencies = [ { name = "wit-bindgen"; @@ -12347,9 +12398,9 @@ rec { }; "wasm-bindgen" = rec { crateName = "wasm-bindgen"; - version = "0.2.118"; + version = "0.2.125"; edition = "2021"; - sha256 = "129s5r14fx4v4xrzpx2c6l860nkxpl48j50y7kl6j16bpah3iy8b"; + sha256 = "06nakz7nfy0ymyp7a27wfbjwx69659i12117hkgddkiv2iwkznwd"; libName = "wasm_bindgen"; authors = [ "The wasm-bindgen Developers" @@ -12398,9 +12449,9 @@ rec { }; "wasm-bindgen-futures" = rec { crateName = "wasm-bindgen-futures"; - version = "0.4.68"; + version = "0.4.75"; edition = "2021"; - sha256 = "1y7bq5d9fk7s9xaayx38bgs9ns35na0kpb5zw19944zvya1x6wgk"; + sha256 = "104jssshr6cm5hmkn6c66mbkyxgaaphng6c17g0dmj7jhk918fsh"; libName = "wasm_bindgen_futures"; authors = [ "The wasm-bindgen Developers" @@ -12410,7 +12461,6 @@ rec { name = "js-sys"; packageId = "js-sys"; usesDefaultFeatures = false; - features = [ "futures" ]; } { name = "wasm-bindgen"; @@ -12427,9 +12477,9 @@ rec { }; "wasm-bindgen-macro" = rec { crateName = "wasm-bindgen-macro"; - version = "0.2.118"; + version = "0.2.125"; edition = "2021"; - sha256 = "1v98r8vs17cj8918qsg0xx4nlg4nxk1g0jd4nwnyrh1687w29zzf"; + sha256 = "0g9w68dwcs4ylm5kxf7schi0kjdfarhc9qlnf8arxc9zn62a28af"; procMacro = true; libName = "wasm_bindgen_macro"; authors = [ @@ -12451,9 +12501,9 @@ rec { }; "wasm-bindgen-macro-support" = rec { crateName = "wasm-bindgen-macro-support"; - version = "0.2.118"; + version = "0.2.125"; edition = "2021"; - sha256 = "0169jr0q469hfx5zqxfyywf2h2f4aj17vn4zly02nfwqmxghc24x"; + sha256 = "1gayzdx5iwl8gllh7ys79wg9cf4iyasl9hrzzhh5m4xx6nfgvkpy"; libName = "wasm_bindgen_macro_support"; authors = [ "The wasm-bindgen Developers" @@ -12473,7 +12523,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "visit" "visit-mut" "full" "extra-traits" ]; } { @@ -12487,10 +12537,10 @@ rec { }; "wasm-bindgen-shared" = rec { crateName = "wasm-bindgen-shared"; - version = "0.2.118"; + version = "0.2.125"; edition = "2021"; links = "wasm_bindgen"; - sha256 = "0ag1vvdzi4334jlzilsy14y3nyzwddf1ndn62fyhf6bg62g4vl2z"; + sha256 = "07w7fy5qa14ys3p8v2p84h98yqinw713smibz9v7apcspd29x4r3"; libName = "wasm_bindgen_shared"; authors = [ "The wasm-bindgen Developers" @@ -12505,9 +12555,9 @@ rec { }; "web-sys" = rec { crateName = "web-sys"; - version = "0.3.95"; + version = "0.3.102"; edition = "2021"; - sha256 = "0zfr2jy5bpkkggl88i43yy37p538hg20i56kwn421yj9g6qznbag"; + sha256 = "0786aybrnwsgdmcynhc2k5ii291a02rq9zk054j35csyvxr0lhx6"; libName = "web_sys"; authors = [ "The wasm-bindgen Developers" @@ -12591,6 +12641,7 @@ rec { "CssStyleSheet" = [ "StyleSheet" ]; "CssSupportsRule" = [ "CssConditionRule" "CssGroupingRule" "CssRule" ]; "CssTransition" = [ "Animation" "EventTarget" ]; + "CssViewTransitionRule" = [ "CssRule" ]; "CustomEvent" = [ "Event" ]; "DedicatedWorkerGlobalScope" = [ "EventTarget" "WorkerGlobalScope" ]; "DelayNode" = [ "AudioNode" "EventTarget" ]; @@ -13081,7 +13132,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; usesDefaultFeatures = false; features = [ "parsing" "proc-macro" "printing" "full" "clone-impls" ]; } @@ -13108,7 +13159,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; usesDefaultFeatures = false; features = [ "parsing" "proc-macro" "printing" "full" "clone-impls" ]; } @@ -13667,7 +13718,7 @@ rec { "Win32_Web" = [ "Win32" ]; "Win32_Web_InternetExplorer" = [ "Win32_Web" ]; }; - resolvedDefaultFeatures = [ "Wdk" "Wdk_Foundation" "Wdk_Storage" "Wdk_Storage_FileSystem" "Wdk_System" "Wdk_System_IO" "Win32" "Win32_Foundation" "Win32_Networking" "Win32_Networking_WinSock" "Win32_Security" "Win32_Security_Authentication" "Win32_Security_Authentication_Identity" "Win32_Security_Credentials" "Win32_Security_Cryptography" "Win32_Storage" "Win32_Storage_FileSystem" "Win32_System" "Win32_System_Console" "Win32_System_Diagnostics" "Win32_System_Diagnostics_Debug" "Win32_System_IO" "Win32_System_LibraryLoader" "Win32_System_Memory" "Win32_System_Pipes" "Win32_System_SystemInformation" "Win32_System_SystemServices" "Win32_System_Threading" "Win32_System_Time" "Win32_System_WindowsProgramming" "default" ]; + resolvedDefaultFeatures = [ "Wdk" "Wdk_Foundation" "Wdk_Storage" "Wdk_Storage_FileSystem" "Wdk_System" "Wdk_System_IO" "Win32" "Win32_Foundation" "Win32_Networking" "Win32_Networking_WinSock" "Win32_Security" "Win32_Security_Authentication" "Win32_Security_Authentication_Identity" "Win32_Security_Credentials" "Win32_Security_Cryptography" "Win32_Storage" "Win32_Storage_FileSystem" "Win32_System" "Win32_System_Console" "Win32_System_Diagnostics" "Win32_System_Diagnostics_Debug" "Win32_System_IO" "Win32_System_LibraryLoader" "Win32_System_Memory" "Win32_System_Pipes" "Win32_System_SystemInformation" "Win32_System_SystemServices" "Win32_System_Threading" "Win32_System_WindowsProgramming" "default" ]; }; "windows-targets" = rec { crateName = "windows-targets"; @@ -13804,9 +13855,9 @@ rec { }; "winnow" = rec { crateName = "winnow"; - version = "1.0.2"; + version = "1.0.3"; edition = "2021"; - sha256 = "1l7xnfvlgy4da6gq5ip2bgcm8i9d0rwzaxg1p88nlw8lxy5p1q9f"; + sha256 = "1wajycd3krn6h699vydjv7hm0ll5l31p899qzpk59y2is74y34h5"; dependencies = [ { name = "memchr"; @@ -13918,9 +13969,9 @@ rec { }; "xml" = rec { crateName = "xml"; - version = "1.2.1"; + version = "1.3.0"; edition = "2021"; - sha256 = "0ak4k990faralbli5a0rb8kvwihccb2rp0r94d4azfy94a6lkamq"; + sha256 = "128s58qhq8whrx90zbw8r5algr7lakgbf7mn05jfk234rbjqavv3"; authors = [ "Vladimir Matveev " "Kornel (https://github.com/kornelski)" @@ -13929,9 +13980,9 @@ rec { }; "yoke" = rec { crateName = "yoke"; - version = "0.8.2"; + version = "0.8.3"; edition = "2021"; - sha256 = "1jprcs7a98a5whvfs6r3jvfh1nnfp6zyijl7y4ywmn88lzywbs5b"; + sha256 = "1xgyj6c2lxj2bp891ynmhws87c6z7yyv2li1v0ss9di40hxf57vh"; authors = [ "Manish Goregaokar " ]; @@ -13983,7 +14034,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "fold" ]; } { @@ -13995,9 +14046,9 @@ rec { }; "zerocopy" = rec { crateName = "zerocopy"; - version = "0.8.48"; + version = "0.8.52"; edition = "2021"; - sha256 = "1sb8plax8jbrsng1jdval7bdhk7hhrx40dz3hwh074k6knzkgm7f"; + sha256 = "0gv563swc1yn3k8w3wjj07a8q293rkx99nfp3a25vzzmbycj446f"; authors = [ "Joshua Liebow-Feeser " "Jack Wrenn " @@ -14031,9 +14082,9 @@ rec { }; "zerocopy-derive" = rec { crateName = "zerocopy-derive"; - version = "0.8.48"; + version = "0.8.52"; edition = "2021"; - sha256 = "1m5s0g92cxggqc74j83k1priz24k3z93sj5gadppd20p9c4cvqvh"; + sha256 = "0c3rhsh4sd9kdym4z55zprybjkydy9y2gvw75d72aapcfa5z7rqs"; procMacro = true; libName = "zerocopy_derive"; authors = [ @@ -14051,14 +14102,14 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" ]; } ]; devDependencies = [ { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "visit" ]; } ]; @@ -14066,11 +14117,11 @@ rec { }; "zerofrom" = rec { crateName = "zerofrom"; - version = "0.1.7"; + version = "0.1.8"; edition = "2021"; - sha256 = "1py40in4rirc9q8w36q67pld0zk8ssg024xhh0cncxgal7ra3yk9"; + sha256 = "0wjjdj7gdmd0iq91gzkxl7dlv0nhkk80l4bmdpzh3a1yh48mmh0f"; authors = [ - "Manish Goregaokar " + "The ICU4X Project Developers" ]; dependencies = [ { @@ -14107,7 +14158,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "fold" ]; } { @@ -14119,9 +14170,9 @@ rec { }; "zeroize" = rec { crateName = "zeroize"; - version = "1.8.2"; - edition = "2021"; - sha256 = "1l48zxgcv34d7kjskr610zqsm6j2b4fcr2vfh9jm9j1jgvk58wdr"; + version = "1.9.0"; + edition = "2024"; + sha256 = "0kpnij2v1ig6g2mhc0bnci0lrdfdhiq40afbc0fahajqc9jiag71"; authors = [ "The RustCrypto Project Developers" ]; @@ -14143,9 +14194,9 @@ rec { }; "zeroize_derive" = rec { crateName = "zeroize_derive"; - version = "1.4.3"; - edition = "2021"; - sha256 = "0bl5vd1lz27p4z336nximg5wrlw5j7jc8fxh7iv6r1wrhhav99c5"; + version = "1.5.0"; + edition = "2024"; + sha256 = "0a7kq8srk81pn23xqn7c9jw1jpnfy41ffn802x1zrqqgpdf6al1w"; procMacro = true; authors = [ "The RustCrypto Project Developers" @@ -14161,7 +14212,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" "extra-traits" "visit" ]; } ]; @@ -14274,7 +14325,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "extra-traits" ]; } ]; diff --git a/Cargo.toml b/Cargo.toml index 3647c4ba..d15d6664 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,6 @@ edition = "2024" repository = "https://github.com/stackabletech/kafka-operator" [workspace.dependencies] -product-config = { git = "https://github.com/stackabletech/product-config.git", tag = "0.8.0" } stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git", tag = "stackable-operator-0.111.1", features = ["crds", "webhook"] } anyhow = "1.0" @@ -30,5 +29,6 @@ tokio = { version = "1.40", features = ["full"] } tracing = "0.1" [patch."https://github.com/stackabletech/operator-rs.git"] +stackable-operator = { git = "https://github.com/stackabletech//operator-rs.git", branch = "smooth-operator" } # stackable-operator = { path = "../operator-rs/crates/stackable-operator" } # stackable-operator = { git = "https://github.com/stackabletech//operator-rs.git", branch = "main" } diff --git a/crate-hashes.json b/crate-hashes.json index 86f2b840..0ff88d85 100644 --- a/crate-hashes.json +++ b/crate-hashes.json @@ -1,12 +1,12 @@ { - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#k8s-version@0.1.3": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-certs@0.4.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-operator-derive@0.3.1": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-operator@0.111.1": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-shared@0.1.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-telemetry@0.6.3": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-versioned-macros@0.10.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-versioned@0.10.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-webhook@0.9.1": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#k8s-version@0.1.3": "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-certs@0.4.0": "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-operator-derive@0.3.1": "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-operator@0.111.1": "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-shared@0.1.1": "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-telemetry@0.6.4": "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-versioned-macros@0.10.0": "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-versioned@0.10.0": "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-webhook@0.9.1": "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp", "git+https://github.com/stackabletech/product-config.git?tag=0.8.0#product-config@0.8.0": "1dz70kapm2wdqcr7ndyjji0lhsl98bsq95gnb2lw487wf6yr7987" } \ No newline at end of file diff --git a/deploy/config-spec/properties.yaml b/deploy/config-spec/properties.yaml index b6f80cdd..9bd8c3b2 100644 --- a/deploy/config-spec/properties.yaml +++ b/deploy/config-spec/properties.yaml @@ -1,152 +1,5 @@ --- version: 0.1.0 spec: - units: - - unit: &unitPort - name: "port" - regex: "^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$" - - - unit: &unitUrl - name: "url" - regex: "^((https?|ftp|file)://)?[-a-zA-Z0-9+&@#}/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]" - examples: - - "https://www.stackable.de/blog/" - - - unit: &unitCapacity - name: "capacity" - regex: "^[1-9]\\d*$" - - - unit: &unitMilliseconds - name: "milliseconds" - regex: "^[1-9]\\d*$" - -properties: - - property: - propertyNames: - - name: "networkaddress.cache.ttl" - kind: - type: "file" - file: "security.properties" - datatype: - type: "integer" - min: "0" - recommendedValues: - - fromVersion: "0.0.0" - value: "30" - roles: - - name: "broker" - required: true - - name: "controller" - required: true - asOfVersion: "0.0.0" - comment: "TTL for successfully resolved domain names." - description: "TTL for successfully resolved domain names." - - - property: - propertyNames: - - name: "networkaddress.cache.negative.ttl" - kind: - type: "file" - file: "security.properties" - datatype: - type: "integer" - min: "0" - recommendedValues: - - fromVersion: "0.0.0" - value: "0" - roles: - - name: "broker" - required: true - - name: "controller" - required: true - asOfVersion: "0.0.0" - comment: "TTL for domain names that cannot be resolved." - description: "TTL for domain names that cannot be resolved." - - - property: &opaAuthorizerClassName - propertyNames: - - name: "authorizer.class.name" - kind: - type: "file" - file: "broker.properties" - datatype: - type: "string" - defaultValues: - - fromVersion: "0.0.0" - value: "com.bisnode.kafka.authorization.OpaAuthorizer" - - fromVersion: "3.0.0" - value: "org.openpolicyagent.kafka.OpaAuthorizer" - roles: - - name: "broker" - required: false - asOfVersion: "0.0.0" - description: "OPA Authorizer class name" - - - property: &opaAuthorizerUrl - propertyNames: - - name: "opa.authorizer.url" - kind: - type: "file" - file: "broker.properties" - datatype: - type: "string" - unit: *unitUrl - roles: - - name: "broker" - required: false - asOfVersion: "0.0.0" - description: "OPA Authorizer URL" - - - property: &opaAuthorizerInitialCacheCapacity - propertyNames: - - name: "opa.authorizer.cache.initial.capacity" - kind: - type: "file" - file: "broker.properties" - datatype: - type: "integer" - unit: *unitCapacity - defaultValues: - - fromVersion: "0.0.0" - value: "0" - roles: - - name: "broker" - required: false - asOfVersion: "0.0.0" - description: "OPA Authorizer initial cache capacity" - - - property: &opaAuthorizerMaxCacheSize - propertyNames: - - name: "opa.authorizer.cache.maximum.size" - kind: - type: "file" - file: "broker.properties" - datatype: - type: "integer" - unit: *unitCapacity - defaultValues: - - fromVersion: "0.0.0" - value: "0" - roles: - - name: "broker" - required: false - asOfVersion: "0.0.0" - description: "OPA authorizer max cache size" - - - property: &opaAuthorizerCacheExpireAfterSeconds - propertyNames: - - name: "opa.authorizer.cache.expire.after.seconds" - kind: - type: "file" - file: "broker.properties" - datatype: - type: "integer" - unit: *unitCapacity - defaultValues: - - fromVersion: "0.0.0" - value: "0" - roles: - - name: "broker" - required: false - asOfVersion: "0.0.0" - description: "The number of seconds after which the OPA authorizer cache expires" + units: [] +properties: [] diff --git a/deploy/helm/kafka-operator/configs/properties.yaml b/deploy/helm/kafka-operator/configs/properties.yaml index b6f80cdd..9bd8c3b2 100644 --- a/deploy/helm/kafka-operator/configs/properties.yaml +++ b/deploy/helm/kafka-operator/configs/properties.yaml @@ -1,152 +1,5 @@ --- version: 0.1.0 spec: - units: - - unit: &unitPort - name: "port" - regex: "^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$" - - - unit: &unitUrl - name: "url" - regex: "^((https?|ftp|file)://)?[-a-zA-Z0-9+&@#}/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]" - examples: - - "https://www.stackable.de/blog/" - - - unit: &unitCapacity - name: "capacity" - regex: "^[1-9]\\d*$" - - - unit: &unitMilliseconds - name: "milliseconds" - regex: "^[1-9]\\d*$" - -properties: - - property: - propertyNames: - - name: "networkaddress.cache.ttl" - kind: - type: "file" - file: "security.properties" - datatype: - type: "integer" - min: "0" - recommendedValues: - - fromVersion: "0.0.0" - value: "30" - roles: - - name: "broker" - required: true - - name: "controller" - required: true - asOfVersion: "0.0.0" - comment: "TTL for successfully resolved domain names." - description: "TTL for successfully resolved domain names." - - - property: - propertyNames: - - name: "networkaddress.cache.negative.ttl" - kind: - type: "file" - file: "security.properties" - datatype: - type: "integer" - min: "0" - recommendedValues: - - fromVersion: "0.0.0" - value: "0" - roles: - - name: "broker" - required: true - - name: "controller" - required: true - asOfVersion: "0.0.0" - comment: "TTL for domain names that cannot be resolved." - description: "TTL for domain names that cannot be resolved." - - - property: &opaAuthorizerClassName - propertyNames: - - name: "authorizer.class.name" - kind: - type: "file" - file: "broker.properties" - datatype: - type: "string" - defaultValues: - - fromVersion: "0.0.0" - value: "com.bisnode.kafka.authorization.OpaAuthorizer" - - fromVersion: "3.0.0" - value: "org.openpolicyagent.kafka.OpaAuthorizer" - roles: - - name: "broker" - required: false - asOfVersion: "0.0.0" - description: "OPA Authorizer class name" - - - property: &opaAuthorizerUrl - propertyNames: - - name: "opa.authorizer.url" - kind: - type: "file" - file: "broker.properties" - datatype: - type: "string" - unit: *unitUrl - roles: - - name: "broker" - required: false - asOfVersion: "0.0.0" - description: "OPA Authorizer URL" - - - property: &opaAuthorizerInitialCacheCapacity - propertyNames: - - name: "opa.authorizer.cache.initial.capacity" - kind: - type: "file" - file: "broker.properties" - datatype: - type: "integer" - unit: *unitCapacity - defaultValues: - - fromVersion: "0.0.0" - value: "0" - roles: - - name: "broker" - required: false - asOfVersion: "0.0.0" - description: "OPA Authorizer initial cache capacity" - - - property: &opaAuthorizerMaxCacheSize - propertyNames: - - name: "opa.authorizer.cache.maximum.size" - kind: - type: "file" - file: "broker.properties" - datatype: - type: "integer" - unit: *unitCapacity - defaultValues: - - fromVersion: "0.0.0" - value: "0" - roles: - - name: "broker" - required: false - asOfVersion: "0.0.0" - description: "OPA authorizer max cache size" - - - property: &opaAuthorizerCacheExpireAfterSeconds - propertyNames: - - name: "opa.authorizer.cache.expire.after.seconds" - kind: - type: "file" - file: "broker.properties" - datatype: - type: "integer" - unit: *unitCapacity - defaultValues: - - fromVersion: "0.0.0" - value: "0" - roles: - - name: "broker" - required: false - asOfVersion: "0.0.0" - description: "The number of seconds after which the OPA authorizer cache expires" + units: [] +properties: [] diff --git a/docs/modules/kafka/pages/reference/commandline-parameters.adoc b/docs/modules/kafka/pages/reference/commandline-parameters.adoc index 9059a960..3c66dc41 100644 --- a/docs/modules/kafka/pages/reference/commandline-parameters.adoc +++ b/docs/modules/kafka/pages/reference/commandline-parameters.adoc @@ -2,19 +2,6 @@ This operator accepts the following command line parameters: -== product-config - -*Default value*: `/etc/stackable/kafka-operator/config-spec/properties.yaml` - -*Required*: false - -*Multiple values:* false - -[source] ----- -stackable-kafka-operator run --product-config /foo/bar/properties.yaml ----- - == watch-namespace *Default value*: All namespaces diff --git a/docs/modules/kafka/pages/reference/environment-variables.adoc b/docs/modules/kafka/pages/reference/environment-variables.adoc index cc7dd3a2..d2271300 100644 --- a/docs/modules/kafka/pages/reference/environment-variables.adoc +++ b/docs/modules/kafka/pages/reference/environment-variables.adoc @@ -33,32 +33,6 @@ docker run \ oci.stackable.tech/sdp/kafka-operator:0.0.0-dev ---- -== PRODUCT_CONFIG - -*Default value*: `/etc/stackable/kafka-operator/config-spec/properties.yaml` - -*Required*: false - -*Multiple values*: false - -[source] ----- -export PRODUCT_CONFIG=/foo/bar/properties.yaml -stackable-kafka-operator run ----- - -or via docker: - ----- -docker run \ - --name kafka-operator \ - --network host \ - --env KUBECONFIG=/home/stackable/.kube/config \ - --env PRODUCT_CONFIG=/my/product/config.yaml \ - --mount type=bind,source="$HOME/.kube/config",target="/home/stackable/.kube/config" \ - oci.stackable.tech/sdp/kafka-operator:0.0.0-dev ----- - == WATCH_NAMESPACE *Default value*: All namespaces diff --git a/extra/crds.yaml b/extra/crds.yaml index a2c3b7d9..109c5672 100644 --- a/extra/crds.yaml +++ b/extra/crds.yaml @@ -79,11 +79,17 @@ spec: type: object bootstrapListenerClass: description: The ListenerClass used for bootstrapping new clients. Should use a stable ListenerClass to avoid unnecessary client restarts (such as `cluster-internal` or `external-stable`). + maxLength: 253 + minLength: 1 nullable: true + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string brokerListenerClass: description: The ListenerClass used for connecting to brokers. Should use a direct connection ListenerClass to minimize cost and minimize performance overhead (such as `cluster-internal` or `external-unstable`). + maxLength: 253 + minLength: 1 nullable: true + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string gracefulShutdownTimeout: description: Time period Pods have to gracefully shut down, e.g. `30m`, `1h` or `2d`. Consult the operator documentation for details. @@ -98,86 +104,6 @@ spec: containers: description: Log configuration per container. properties: - get-service: - anyOf: - - required: - - custom - - {} - - {} - description: Log configuration of the container - properties: - console: - description: Configuration for the console appender - nullable: true - properties: - level: - description: |- - The log level threshold. - Log events with a lower log level are discarded. - enum: - - TRACE - - DEBUG - - INFO - - WARN - - ERROR - - FATAL - - NONE - - null - nullable: true - type: string - type: object - custom: - description: Log configuration provided in a ConfigMap - properties: - configMap: - description: ConfigMap containing the log configuration files - nullable: true - type: string - type: object - file: - description: Configuration for the file appender - nullable: true - properties: - level: - description: |- - The log level threshold. - Log events with a lower log level are discarded. - enum: - - TRACE - - DEBUG - - INFO - - WARN - - ERROR - - FATAL - - NONE - - null - nullable: true - type: string - type: object - loggers: - additionalProperties: - description: Configuration of a logger - properties: - level: - description: |- - The log level threshold. - Log events with a lower log level are discarded. - enum: - - TRACE - - DEBUG - - INFO - - WARN - - ERROR - - FATAL - - NONE - - null - nullable: true - type: string - type: object - default: {} - description: Configuration per logger - type: object - type: object kafka: anyOf: - required: @@ -542,22 +468,14 @@ spec: broker.properties: additionalProperties: type: string - description: |- - Flat key-value overrides for `*.properties`, Hadoop XML, etc. - - This is backwards-compatible with the existing flat key-value YAML format - used by `HashMap`. - nullable: true + default: {} + description: Flat key-value overrides for `*.properties`, Hadoop XML, etc. type: object security.properties: additionalProperties: type: string - description: |- - Flat key-value overrides for `*.properties`, Hadoop XML, etc. - - This is backwards-compatible with the existing flat key-value YAML format - used by `HashMap`. - nullable: true + default: {} + description: Flat key-value overrides for `*.properties`, Hadoop XML, etc. type: object type: object envOverrides: @@ -696,11 +614,17 @@ spec: type: object bootstrapListenerClass: description: The ListenerClass used for bootstrapping new clients. Should use a stable ListenerClass to avoid unnecessary client restarts (such as `cluster-internal` or `external-stable`). + maxLength: 253 + minLength: 1 nullable: true + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string brokerListenerClass: description: The ListenerClass used for connecting to brokers. Should use a direct connection ListenerClass to minimize cost and minimize performance overhead (such as `cluster-internal` or `external-unstable`). + maxLength: 253 + minLength: 1 nullable: true + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string gracefulShutdownTimeout: description: Time period Pods have to gracefully shut down, e.g. `30m`, `1h` or `2d`. Consult the operator documentation for details. @@ -715,86 +639,6 @@ spec: containers: description: Log configuration per container. properties: - get-service: - anyOf: - - required: - - custom - - {} - - {} - description: Log configuration of the container - properties: - console: - description: Configuration for the console appender - nullable: true - properties: - level: - description: |- - The log level threshold. - Log events with a lower log level are discarded. - enum: - - TRACE - - DEBUG - - INFO - - WARN - - ERROR - - FATAL - - NONE - - null - nullable: true - type: string - type: object - custom: - description: Log configuration provided in a ConfigMap - properties: - configMap: - description: ConfigMap containing the log configuration files - nullable: true - type: string - type: object - file: - description: Configuration for the file appender - nullable: true - properties: - level: - description: |- - The log level threshold. - Log events with a lower log level are discarded. - enum: - - TRACE - - DEBUG - - INFO - - WARN - - ERROR - - FATAL - - NONE - - null - nullable: true - type: string - type: object - loggers: - additionalProperties: - description: Configuration of a logger - properties: - level: - description: |- - The log level threshold. - Log events with a lower log level are discarded. - enum: - - TRACE - - DEBUG - - INFO - - WARN - - ERROR - - FATAL - - NONE - - null - nullable: true - type: string - type: object - default: {} - description: Configuration per logger - type: object - type: object kafka: anyOf: - required: @@ -1159,22 +1003,14 @@ spec: broker.properties: additionalProperties: type: string - description: |- - Flat key-value overrides for `*.properties`, Hadoop XML, etc. - - This is backwards-compatible with the existing flat key-value YAML format - used by `HashMap`. - nullable: true + default: {} + description: Flat key-value overrides for `*.properties`, Hadoop XML, etc. type: object security.properties: additionalProperties: type: string - description: |- - Flat key-value overrides for `*.properties`, Hadoop XML, etc. - - This is backwards-compatible with the existing flat key-value YAML format - used by `HashMap`. - nullable: true + default: {} + description: Flat key-value overrides for `*.properties`, Hadoop XML, etc. type: object type: object envOverrides: @@ -1329,7 +1165,10 @@ spec: ``` This is necessary when migrating from ZooKeeper to Kraft mode to retain existing broker IDs because previously broker ids were generated by Kafka and not the operator. + maxLength: 253 + minLength: 1 nullable: true + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string metadataManager: description: |- @@ -1380,6 +1219,11 @@ spec: - Which ca.crt to use when validating the other brokers Defaults to `tls` + Set to `null` to disable internal TLS (resulting in a plaintext cluster). + maxLength: 253 + minLength: 1 + nullable: true + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string serverSecretClass: default: tls @@ -1390,7 +1234,10 @@ spec: - Which cert the servers should use to authenticate themselves against the client Defaults to `tls`. + maxLength: 253 + minLength: 1 nullable: true + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string type: object vectorAggregatorConfigMapName: @@ -1399,7 +1246,10 @@ spec: It must contain the key `ADDRESS` with the address of the Vector aggregator. Follow the [logging tutorial](https://docs.stackable.tech/home/nightly/tutorials/logging-vector-aggregator) to learn how to configure log aggregation with Vector. + maxLength: 253 + minLength: 1 nullable: true + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string zookeeperConfigMapName: description: |- @@ -1408,7 +1258,10 @@ spec: to deploy a ZooKeeper cluster, this will simply be the name of your ZookeeperCluster resource. This can only be used up to Kafka version 3.9.x. Since Kafka 4.0.0, ZooKeeper support was dropped. Please use the 'controller' role instead. + maxLength: 253 + minLength: 1 nullable: true + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string type: object clusterOperation: @@ -1787,22 +1640,14 @@ spec: controller.properties: additionalProperties: type: string - description: |- - Flat key-value overrides for `*.properties`, Hadoop XML, etc. - - This is backwards-compatible with the existing flat key-value YAML format - used by `HashMap`. - nullable: true + default: {} + description: Flat key-value overrides for `*.properties`, Hadoop XML, etc. type: object security.properties: additionalProperties: type: string - description: |- - Flat key-value overrides for `*.properties`, Hadoop XML, etc. - - This is backwards-compatible with the existing flat key-value YAML format - used by `HashMap`. - nullable: true + default: {} + description: Flat key-value overrides for `*.properties`, Hadoop XML, etc. type: object type: object envOverrides: @@ -2236,22 +2081,14 @@ spec: controller.properties: additionalProperties: type: string - description: |- - Flat key-value overrides for `*.properties`, Hadoop XML, etc. - - This is backwards-compatible with the existing flat key-value YAML format - used by `HashMap`. - nullable: true + default: {} + description: Flat key-value overrides for `*.properties`, Hadoop XML, etc. type: object security.properties: additionalProperties: type: string - description: |- - Flat key-value overrides for `*.properties`, Hadoop XML, etc. - - This is backwards-compatible with the existing flat key-value YAML format - used by `HashMap`. - nullable: true + default: {} + description: Flat key-value overrides for `*.properties`, Hadoop XML, etc. type: object type: object envOverrides: diff --git a/rust/operator-binary/Cargo.toml b/rust/operator-binary/Cargo.toml index f2903572..b9d9d40f 100644 --- a/rust/operator-binary/Cargo.toml +++ b/rust/operator-binary/Cargo.toml @@ -9,7 +9,6 @@ repository.workspace = true publish = false [dependencies] -product-config.workspace = true stackable-operator.workspace = true indoc.workspace = true diff --git a/rust/operator-binary/src/config/mod.rs b/rust/operator-binary/src/config/mod.rs deleted file mode 100644 index ae92b3c2..00000000 --- a/rust/operator-binary/src/config/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod command; -pub mod jvm; -pub mod node_id_hasher; diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index a064c1ac..6e72ddf5 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -1,57 +1,434 @@ -//! Ensures that `Pod`s are configured and running for each [`v1alpha1::KafkaCluster`]. - -use std::{str::FromStr, sync::Arc}; +//! The validated cluster model, the reconcile loop, and the steps that produce it. +//! +//! [`ValidatedCluster`] carries everything the build steps need, resolved once during +//! [`validate`] (after [`dereference`]) so downstream code never re-derives it or +//! touches the raw [`v1alpha1::KafkaCluster`] spec. [`reconcile_kafka`] consumes it to +//! build (under [`build`]) and apply the child resources. + +use std::{ + borrow::Cow, + collections::{BTreeMap, HashMap}, + str::FromStr, + sync::Arc, +}; use const_format::concatcp; -use product_config::ProductConfigManager; use snafu::{ResultExt, Snafu}; use stackable_operator::{ cli::OperatorEnvironmentOptions, - cluster_resources::{ClusterResourceApplyStrategy, ClusterResources}, - commons::rbac::build_rbac_resources, + cluster_resources::ClusterResourceApplyStrategy, + commons::{networking::DomainName, product_image_selection::ResolvedProductImage}, crd::listener, kube::{ Resource, - api::DynamicObject, + api::{DynamicObject, ObjectMeta}, core::{DeserializeGuard, error_boundary}, runtime::{controller::Action, reflector::ObjectRef}, }, + kvp::Labels, logging::controller::ReconcilerError, - role_utils::{GenericRoleConfig, RoleGroupRef}, shared::time::Duration, status::condition::{ compute_conditions, operations::ClusterOperationsConditionBuilder, statefulset::StatefulSetConditionBuilder, }, + v2::{ + HasName, HasUid, NameIsValidLabelValue, + cluster_resources::cluster_resources_new, + kvp::label::{recommended_labels, role_group_selector}, + role_group_utils::ResourceNames, + types::{ + kubernetes::{ConfigMapName, ListenerName, NamespaceName, Uid}, + operator::{ClusterName, ControllerName, OperatorName, ProductName, ProductVersion}, + }, + }, }; use strum::{EnumDiscriminants, IntoStaticStr}; -mod dereference; -mod validate; +pub(crate) mod build; +pub(crate) mod dereference; +pub(crate) mod node_id_hasher; +pub(crate) mod security; +pub(crate) mod validate; + +/// The type-safe role-group name from stackable-operator. Re-exported so the rest +/// of the operator can refer to it as `controller::RoleGroupName`. +pub use stackable_operator::v2::types::operator::{RoleGroupName, RoleName}; use crate::{ + controller::{ + build::{ + properties::listener::get_kafka_listener_config, + resource::{ + listener::build_broker_rolegroup_bootstrap_listener, + pdb::build_pdb, + rbac::{build_rbac_role_binding, build_rbac_service_account}, + service::{build_rolegroup_headless_service, build_rolegroup_metrics_service}, + statefulset::{ + build_broker_rolegroup_statefulset, build_controller_rolegroup_statefulset, + }, + }, + }, + node_id_hasher::node_id_hash32_offset, + security::ValidatedKafkaSecurity, + }, crd::{ - self, APP_NAME, KafkaClusterStatus, OPERATOR_NAME, - listener::get_kafka_listener_config, - role::{AnyConfig, KafkaRole}, + APP_NAME, KafkaClusterStatus, KafkaPodDescriptor, MetadataManager, OPERATOR_NAME, + authorization::KafkaAuthorizationConfig, + role::{AnyConfig, AnyConfigOverrides, KafkaRole}, v1alpha1, }, - discovery::{self, build_discovery_configmap}, - operations::pdb::add_pdbs, - resource::{ - configmap::build_rolegroup_config_map, - listener::build_broker_rolegroup_bootstrap_listener, - service::{build_rolegroup_headless_service, build_rolegroup_metrics_service}, - statefulset::{build_broker_rolegroup_statefulset, build_controller_rolegroup_statefulset}, - }, }; pub const KAFKA_CONTROLLER_NAME: &str = "kafkacluster"; pub const KAFKA_FULL_CONTROLLER_NAME: &str = concatcp!(KAFKA_CONTROLLER_NAME, '.', OPERATOR_NAME); +#[derive(Snafu, Debug)] +pub enum PodDescriptorsError { + #[snafu(display( + "the node id hash offset of role group {role}/{role_group} collides with {colliding_role}/{colliding_role_group}; node ids must be unique across the cluster" + ))] + KafkaNodeIdHashCollision { + role: KafkaRole, + role_group: RoleGroupName, + colliding_role: KafkaRole, + colliding_role_group: RoleGroupName, + }, +} + +/// The validated cluster. Carries everything the build steps need, resolved once +/// here so downstream code never re-derives it or touches the raw spec. +/// +/// The cluster identity (`name`, `namespace`, `uid`) is captured here so that owner +/// references for child objects can be built straight from this struct (via its +/// [`Resource`] impl) without threading the raw [`v1alpha1::KafkaCluster`] around. +pub struct ValidatedCluster { + /// `ObjectMeta` carrying `name`, `namespace` and `uid`, so this struct can act as the + /// owner [`Resource`] for child objects. + metadata: ObjectMeta, + pub name: ClusterName, + pub namespace: NamespaceName, + pub uid: Uid, + /// The Kubernetes cluster domain (e.g. `cluster.local`), resolved from the operator's + /// `KubernetesClusterInfo`. Used to compute pod FQDNs in [`Self::pod_descriptors`]. + pub cluster_domain: DomainName, + pub image: ResolvedProductImage, + /// The product version as a valid label value, used for the recommended + /// `app.kubernetes.io/version` label. Derived from the resolved image's app version label + /// value. + pub product_version: ProductVersion, + pub cluster_config: ValidatedClusterConfig, + /// Per-role configuration (e.g. the Pod disruption budget), keyed by role. + pub role_configs: BTreeMap, + pub role_group_configs: BTreeMap>, +} + +impl ValidatedCluster { + #[allow(clippy::too_many_arguments)] + pub fn new( + name: ClusterName, + namespace: NamespaceName, + uid: Uid, + cluster_domain: DomainName, + image: ResolvedProductImage, + cluster_config: ValidatedClusterConfig, + role_configs: BTreeMap, + role_group_configs: BTreeMap>, + ) -> Self { + // `app_version_label_value` is constructed to be a valid label value, so it is also a + // valid `ProductVersion`. + let product_version = ProductVersion::from_str(&image.app_version_label_value) + .expect("the app version label value is a valid product version"); + Self { + metadata: ObjectMeta { + name: Some(name.to_string()), + namespace: Some(namespace.to_string()), + uid: Some(uid.to_string()), + ..ObjectMeta::default() + }, + name, + namespace, + uid, + cluster_domain, + image, + product_version, + cluster_config, + role_configs, + role_group_configs, + } + } + + /// Predicts the pods of this cluster (or just `requested_kafka_role`'s pods, if given). + /// + /// Pods are predicted rather than read from the live cluster to avoid instance churn. The + /// node-id hash offsets must be unique across the whole cluster, so collisions are detected + /// across all role groups regardless of `requested_kafka_role`. + /// + /// Resource names reuse [`Self::resource_names`] (the canonical + /// `--` naming) so they stay in sync with the StatefulSet and + /// headless Service this descriptor refers to. + pub fn pod_descriptors( + &self, + requested_kafka_role: Option<&KafkaRole>, + ) -> Result, PodDescriptorsError> { + let client_port = self.cluster_config.kafka_security.client_port(); + let mut pod_descriptors = Vec::new(); + let mut seen_hashes = HashMap::::new(); + + for (role, role_groups) in &self.role_group_configs { + for (role_group_name, validated_rg) in role_groups { + let node_id_hash_offset = node_id_hash32_offset(role, role_group_name.as_ref()); + + // The node id hash offset must be unique across the cluster. + if let Some((colliding_role, colliding_role_group)) = + seen_hashes.get(&node_id_hash_offset) + { + return KafkaNodeIdHashCollisionSnafu { + role: role.clone(), + role_group: role_group_name.clone(), + colliding_role: colliding_role.clone(), + colliding_role_group: colliding_role_group.clone(), + } + .fail(); + } + seen_hashes.insert(node_id_hash_offset, (role.clone(), role_group_name.clone())); + + if requested_kafka_role.is_none() || Some(role) == requested_kafka_role { + let resource_names = self.resource_names(role, role_group_name); + let role_group_statefulset_name = resource_names.stateful_set_name(); + let role_group_service_name = resource_names.headless_service_name(); + // Pods must be predicted from a concrete count (e.g. for KRaft quorum + // voters), so an unset replica count falls back to 1. + for replica in 0..validated_rg.replicas.unwrap_or(1) { + pod_descriptors.push(KafkaPodDescriptor { + namespace: self.namespace.clone(), + role_group_service_name: role_group_service_name.clone(), + role_group_statefulset_name: role_group_statefulset_name.clone(), + replica, + cluster_domain: self.cluster_domain.clone(), + node_id: node_id_hash_offset + u32::from(replica), + role: role.clone(), + client_port: client_port.clone(), + }); + } + } + } + } + + Ok(pod_descriptors) + } + + /// The given [`KafkaRole`] as a type-safe [`RoleName`]. + pub fn role_name(role: &KafkaRole) -> RoleName { + RoleName::from_str(&role.to_string()).expect("a KafkaRole is a valid role name") + } + + /// Type-safe names for the resources of a given role group. + pub(crate) fn resource_names( + &self, + role: &KafkaRole, + role_group_name: &RoleGroupName, + ) -> ResourceNames { + ResourceNames { + cluster_name: self.name.clone(), + role_name: Self::role_name(role), + role_group_name: role_group_name.clone(), + } + } + + /// The name of the broker rolegroup's bootstrap [`Listener`](stackable_operator::crd::listener), + /// `---bootstrap`. + pub fn bootstrap_listener_name( + &self, + role: &KafkaRole, + role_group_name: &RoleGroupName, + ) -> ListenerName { + ListenerName::from_str(&format!( + "{}-bootstrap", + self.resource_names(role, role_group_name) + .stateful_set_name() + )) + .expect("the bootstrap listener name is a valid Listener name") + } + + /// Recommended labels for a role-group resource, using the given product version. + fn recommended_labels_for( + &self, + product_version: &ProductVersion, + role: &KafkaRole, + role_group_name: &RoleGroupName, + ) -> Labels { + recommended_labels( + self, + &product_name(), + product_version, + &operator_name(), + &controller_name(), + &Self::role_name(role), + role_group_name, + ) + } + + /// Recommended labels for a role-group resource. + pub fn recommended_labels(&self, role: &KafkaRole, role_group_name: &RoleGroupName) -> Labels { + self.recommended_labels_for(&self.product_version, role, role_group_name) + } + + /// Recommended labels without a version, for PVC templates that cannot be modified once + /// deployed. + pub fn unversioned_recommended_labels( + &self, + role: &KafkaRole, + role_group_name: &RoleGroupName, + ) -> Labels { + // A version value is required, and we do want to use the "recommended" format for the + // other desired labels. + let none_version = + ProductVersion::from_str("none").expect("'none' is a valid product version"); + self.recommended_labels_for(&none_version, role, role_group_name) + } + + /// Selector labels matching the pods of a role group. + pub fn role_group_selector(&self, role: &KafkaRole, role_group_name: &RoleGroupName) -> Labels { + role_group_selector( + self, + &product_name(), + &Self::role_name(role), + role_group_name, + ) + } +} + +impl HasName for ValidatedCluster { + fn to_name(&self) -> String { + self.name.to_string() + } +} + +impl HasUid for ValidatedCluster { + fn to_uid(&self) -> Uid { + self.uid.clone() + } +} + +impl NameIsValidLabelValue for ValidatedCluster { + fn to_label_value(&self) -> String { + self.name.to_label_value() + } +} + +/// The product name (`kafka`) as a type-safe label value. +pub(crate) fn product_name() -> ProductName { + ProductName::from_str(APP_NAME).expect("'kafka' is a valid product name") +} + +/// The operator name as a type-safe label value. +pub(crate) fn operator_name() -> OperatorName { + OperatorName::from_str(OPERATOR_NAME).expect("the operator name is a valid label value") +} + +/// The controller name as a type-safe label value. +pub(crate) fn controller_name() -> ControllerName { + ControllerName::from_str(KAFKA_CONTROLLER_NAME) + .expect("the controller name is a valid label value") +} + +/// Cluster-wide settings resolved during validation and dereferencing. +/// +/// Everything the build steps need is resolved here so they never have to read the +/// raw [`v1alpha1::KafkaCluster`] spec. +pub struct ValidatedClusterConfig { + pub kafka_security: ValidatedKafkaSecurity, + pub authorization_config: Option, + pub metadata_manager: MetadataManager, + + /// The discovery `ConfigMap` providing the ZooKeeper connection string, if the cluster + /// is connected to a ZooKeeper ensemble. Resolved from the raw spec during validation so + /// the build steps never have to read it. + pub zookeeper_config_map_name: Option, + + /// The `ConfigMap` mapping pods to broker ids, if the user supplied one. Resolved from the + /// raw spec during validation so the build steps never have to read it. + pub broker_id_pod_config_map_name: Option, +} + +impl ValidatedClusterConfig { + /// Whether the cluster runs in KRaft mode (as opposed to ZooKeeper mode). + pub fn is_kraft_mode(&self) -> bool { + self.metadata_manager == MetadataManager::KRaft + } + + /// Whether the operator must not generate broker ids itself, because the user supplied a + /// `broker_id_pod_config_map_name`. + pub fn disable_broker_id_generation(&self) -> bool { + self.broker_id_pod_config_map_name.is_some() + } + + /// The OPA connect string, if OPA authorization is configured. + pub fn opa_connect(&self) -> Option<&str> { + self.authorization_config + .as_ref() + .map(|auth_config| auth_config.opa_connect.as_str()) + } +} + +/// Per-role configuration extracted during validation. +/// +/// Resolved from the raw [`v1alpha1::KafkaCluster`] spec during validation so the reconcile loop +/// never has to read it. Kafka's `GenericRoleConfig` only carries the Pod disruption budget. +#[derive(Clone, Debug)] +pub struct ValidatedRoleConfig { + pub pdb: stackable_operator::commons::pdb::PdbConfig, +} + +/// Lets [`ValidatedCluster`] act as the owner [`Resource`] for child objects, so owner +/// references are built from it (via the captured `metadata`) rather than the raw CR. +impl Resource for ValidatedCluster { + type DynamicType = ::DynamicType; + type Scope = ::Scope; + + fn kind(dt: &Self::DynamicType) -> Cow<'_, str> { + v1alpha1::KafkaCluster::kind(dt) + } + + fn group(dt: &Self::DynamicType) -> Cow<'_, str> { + v1alpha1::KafkaCluster::group(dt) + } + + fn version(dt: &Self::DynamicType) -> Cow<'_, str> { + v1alpha1::KafkaCluster::version(dt) + } + + fn plural(dt: &Self::DynamicType) -> Cow<'_, str> { + v1alpha1::KafkaCluster::plural(dt) + } + + fn meta(&self) -> &ObjectMeta { + &self.metadata + } + + fn meta_mut(&mut self) -> &mut ObjectMeta { + &mut self.metadata + } +} + +/// The validated, merged per-role-group product config. +#[derive(Clone, Debug, PartialEq)] +pub struct ValidatedKafkaConfig { + pub config: AnyConfig, + /// Validated logging configuration (derived from `config.logging` during validation). + pub logging: validate::ValidatedLogging, +} + +/// A validated, merged Kafka role-group config. +pub type ValidatedRoleGroupConfig = stackable_operator::v2::role_utils::RoleGroupConfig< + ValidatedKafkaConfig, + stackable_operator::v2::role_utils::JavaCommonConfig, + AnyConfigOverrides, +>; + pub struct Ctx { pub client: stackable_operator::client::Client, - pub product_config: ProductConfigManager, pub operator_environment: OperatorEnvironmentOptions, } @@ -65,39 +442,33 @@ pub enum Error { #[snafu(display("failed to validate cluster"))] ValidateCluster { source: validate::Error }, - #[snafu(display("failed to build pod descriptors"))] - BuildPodDescriptors { source: crate::crd::Error }, - - #[snafu(display("invalid kafka listeners"))] - InvalidKafkaListeners { - source: crate::crd::listener::KafkaListenerError, - }, - - #[snafu(display("failed to apply role Service"))] - ApplyRoleService { + #[snafu(display("failed to apply bootstrap Listener"))] + ApplyBootstrapListener { source: stackable_operator::cluster_resources::Error, }, - #[snafu(display("failed to apply Service for {}", rolegroup))] + #[snafu(display("failed to apply Service for role group {role_group}"))] ApplyRoleGroupService { source: stackable_operator::cluster_resources::Error, - rolegroup: RoleGroupRef, + role_group: RoleGroupName, }, - #[snafu(display("failed to apply ConfigMap for {}", rolegroup))] + #[snafu(display("failed to apply ConfigMap for role group {role_group}"))] ApplyRoleGroupConfig { source: stackable_operator::cluster_resources::Error, - rolegroup: RoleGroupRef, + role_group: RoleGroupName, }, - #[snafu(display("failed to apply StatefulSet for {}", rolegroup))] + #[snafu(display("failed to apply StatefulSet for role group {role_group}"))] ApplyRoleGroupStatefulSet { source: stackable_operator::cluster_resources::Error, - rolegroup: RoleGroupRef, + role_group: RoleGroupName, }, #[snafu(display("failed to build discovery ConfigMap"))] - BuildDiscoveryConfig { source: discovery::Error }, + BuildDiscoveryConfig { + source: build::resource::discovery::Error, + }, #[snafu(display("failed to apply discovery ConfigMap"))] ApplyDiscoveryConfig { @@ -109,14 +480,6 @@ pub enum Error { source: stackable_operator::cluster_resources::Error, }, - #[snafu(display("failed to create cluster resources"))] - CreateClusterResources { - source: stackable_operator::cluster_resources::Error, - }, - - #[snafu(display("failed to resolve and merge config for role and role group"))] - FailedToResolveConfig { source: crate::crd::role::Error }, - #[snafu(display("failed to patch service account"))] ApplyServiceAccount { source: stackable_operator::cluster_resources::Error, @@ -132,14 +495,9 @@ pub enum Error { source: stackable_operator::client::Error, }, - #[snafu(display("failed to build RBAC resources"))] - BuildRbacResources { - source: stackable_operator::commons::rbac::Error, - }, - - #[snafu(display("failed to create PodDisruptionBudget"))] - FailedToCreatePdb { - source: crate::operations::pdb::Error, + #[snafu(display("failed to apply PodDisruptionBudget"))] + ApplyPdb { + source: stackable_operator::cluster_resources::Error, }, #[snafu(display("failed to get required Labels"))] @@ -153,30 +511,14 @@ pub enum Error { source: error_boundary::InvalidObject, }, - #[snafu(display("KafkaCluster object is misconfigured"))] - MisconfiguredKafkaCluster { source: crd::Error }, - - #[snafu(display("failed to parse role: {source}"))] - ParseRole { source: strum::ParseError }, - #[snafu(display("failed to build statefulset"))] BuildStatefulset { - source: crate::resource::statefulset::Error, + source: crate::controller::build::resource::statefulset::Error, }, #[snafu(display("failed to build configmap"))] BuildConfigMap { - source: crate::resource::configmap::Error, - }, - - #[snafu(display("failed to build service"))] - BuildService { - source: crate::resource::service::Error, - }, - - #[snafu(display("failed to build listener"))] - BuildListener { - source: crate::resource::listener::Error, + source: crate::controller::build::resource::config_map::Error, }, } type Result = std::result::Result; @@ -190,30 +532,21 @@ impl ReconcilerError for Error { match self { Error::Dereference { .. } => None, Error::ValidateCluster { .. } => None, - Error::ApplyRoleService { .. } => None, + Error::ApplyBootstrapListener { .. } => None, Error::ApplyRoleGroupService { .. } => None, Error::ApplyRoleGroupConfig { .. } => None, Error::ApplyRoleGroupStatefulSet { .. } => None, Error::BuildDiscoveryConfig { .. } => None, Error::ApplyDiscoveryConfig { .. } => None, Error::DeleteOrphans { .. } => None, - Error::CreateClusterResources { .. } => None, - Error::FailedToResolveConfig { .. } => None, Error::ApplyServiceAccount { .. } => None, Error::ApplyRoleBinding { .. } => None, Error::ApplyStatus { .. } => None, - Error::BuildRbacResources { .. } => None, - Error::FailedToCreatePdb { .. } => None, + Error::ApplyPdb { .. } => None, Error::GetRequiredLabels { .. } => None, Error::InvalidKafkaCluster { .. } => None, - Error::MisconfiguredKafkaCluster { .. } => None, - Error::ParseRole { .. } => None, Error::BuildStatefulset { .. } => None, Error::BuildConfigMap { .. } => None, - Error::BuildService { .. } => None, - Error::BuildListener { .. } => None, - Error::InvalidKafkaListeners { .. } => None, - Error::BuildPodDescriptors { .. } => None, } } } @@ -238,51 +571,36 @@ pub async fn reconcile_kafka( .context(DereferenceSnafu)?; // validate (no client required) - let validate::ValidatedInputs { - authorization_config, - image, - kafka_security, - role_config: validated_config, - } = validate::validate( - kafka, - dereferenced_objects, - &ctx.operator_environment, - &ctx.product_config, - ) - .context(ValidateClusterSnafu)?; - - let opa_connect = authorization_config - .as_ref() - .map(|auth_config| auth_config.opa_connect.clone()); - - let mut cluster_resources = ClusterResources::new( - APP_NAME, - OPERATOR_NAME, - KAFKA_CONTROLLER_NAME, - &kafka.object_ref(&()), + let validated_cluster = + validate::validate(kafka, dereferenced_objects, &ctx.operator_environment) + .context(ValidateClusterSnafu)?; + + let mut cluster_resources = cluster_resources_new( + &product_name(), + &operator_name(), + &controller_name(), + &validated_cluster.name, + &validated_cluster.namespace, + &validated_cluster.uid, ClusterResourceApplyStrategy::from(&kafka.spec.cluster_operation), &kafka.spec.object_overrides, - ) - .context(CreateClusterResourcesSnafu)?; + ); tracing::debug!( - kerberos_enabled = kafka_security.has_kerberos_enabled(), - kerberos_secret_class = ?kafka_security.kerberos_secret_class(), - tls_enabled = kafka_security.tls_enabled(), - tls_client_authentication_class = ?kafka_security.tls_client_authentication_class(), + kerberos_enabled = validated_cluster.cluster_config.kafka_security.has_kerberos_enabled(), + kerberos_secret_class = ?validated_cluster.cluster_config.kafka_security.kerberos_secret_class(), + tls_enabled = validated_cluster.cluster_config.kafka_security.tls_enabled(), + tls_client_authentication_class = ?validated_cluster.cluster_config.kafka_security.tls_client_authentication_class(), "The following security settings are used" ); let mut ss_cond_builder = StatefulSetConditionBuilder::default(); - let (rbac_sa, rbac_rolebinding) = build_rbac_resources( - kafka, - APP_NAME, - cluster_resources - .get_required_labels() - .context(GetRequiredLabelsSnafu)?, - ) - .context(BuildRbacResourcesSnafu)?; + let required_labels = cluster_resources + .get_required_labels() + .context(GetRequiredLabelsSnafu)?; + let rbac_sa = build_rbac_service_account(&validated_cluster, required_labels.clone()); + let rbac_rolebinding = build_rbac_role_binding(&validated_cluster, required_labels); let rbac_sa = cluster_resources .add(client, rbac_sa.clone()) @@ -295,93 +613,75 @@ pub async fn reconcile_kafka( let mut bootstrap_listeners = Vec::::new(); - for (kafka_role_str, role_config) in &validated_config { - let kafka_role = KafkaRole::from_str(kafka_role_str).context(ParseRoleSnafu)?; - - for (rolegroup_name, rolegroup_config) in role_config.iter() { - let rolegroup_ref = kafka.rolegroup_ref(&kafka_role, rolegroup_name); - - let merged_config = kafka_role - .merged_config(kafka, &rolegroup_ref.role_group) - .context(FailedToResolveConfigSnafu)?; - - let rg_headless_service = - build_rolegroup_headless_service(kafka, &image, &rolegroup_ref, &kafka_security) - .context(BuildServiceSnafu)?; + for (kafka_role, rg_map) in &validated_cluster.role_group_configs { + for (rolegroup_name, validated_rg) in rg_map { + // The Vector agent config is the static `vector.yaml`, added to the rolegroup + // ConfigMap only when the Vector agent is enabled (resolved during validation). + let vector_config = validated_rg + .config + .logging + .vector_container + .is_some() + .then(build::properties::product_logging::vector_config_file_content); + + let rg_headless_service = build_rolegroup_headless_service( + &validated_cluster, + kafka_role, + rolegroup_name, + &validated_cluster.cluster_config.kafka_security, + ); - let rg_metrics_service = build_rolegroup_metrics_service(kafka, &image, &rolegroup_ref) - .context(BuildServiceSnafu)?; + let rg_metrics_service = + build_rolegroup_metrics_service(&validated_cluster, kafka_role, rolegroup_name); let kafka_listeners = get_kafka_listener_config( - kafka, - &kafka_security, - &rolegroup_ref, + &validated_cluster, + &validated_cluster.cluster_config.kafka_security, + kafka_role, + rolegroup_name, &client.kubernetes_cluster_info, - ) - .context(InvalidKafkaListenersSnafu)?; + ); - let pod_descriptors = kafka - .pod_descriptors( - None, - &client.kubernetes_cluster_info, - kafka_security.client_port(), - ) - .context(BuildPodDescriptorsSnafu)?; - - let rg_configmap = build_rolegroup_config_map( - kafka, - &image, - &kafka_security, - &rolegroup_ref, - rolegroup_config, - &merged_config, + let rg_configmap = build::resource::config_map::build_rolegroup_config_map( + &validated_cluster, + rolegroup_name, + validated_rg, &kafka_listeners, - &pod_descriptors, - opa_connect.as_deref(), + vector_config, ) .context(BuildConfigMapSnafu)?; let rg_statefulset = match kafka_role { KafkaRole::Broker => build_broker_rolegroup_statefulset( - kafka, - &kafka_role, - &image, - &rolegroup_ref, - rolegroup_config, - &kafka_security, - &merged_config, + kafka_role, + rolegroup_name, + &validated_cluster, + validated_rg, &rbac_sa, - &client.kubernetes_cluster_info, ) .context(BuildStatefulsetSnafu)?, KafkaRole::Controller => build_controller_rolegroup_statefulset( - kafka, - &kafka_role, - &image, - &rolegroup_ref, - rolegroup_config, - &kafka_security, - &merged_config, + kafka_role, + rolegroup_name, + &validated_cluster, + validated_rg, &rbac_sa, - &client.kubernetes_cluster_info, ) .context(BuildStatefulsetSnafu)?, }; - if let AnyConfig::Broker(broker_config) = merged_config { + if let AnyConfig::Broker(broker_config) = &validated_rg.config.config { let rg_bootstrap_listener = build_broker_rolegroup_bootstrap_listener( - kafka, - &image, - &kafka_security, - &rolegroup_ref, - &broker_config, - ) - .context(BuildListenerSnafu)?; + &validated_cluster, + kafka_role, + rolegroup_name, + broker_config, + ); bootstrap_listeners.push( cluster_resources .add(client, rg_bootstrap_listener) .await - .context(ApplyRoleServiceSnafu)?, + .context(ApplyBootstrapListenerSnafu)?, ); } @@ -389,19 +689,19 @@ pub async fn reconcile_kafka( .add(client, rg_headless_service) .await .with_context(|_| ApplyRoleGroupServiceSnafu { - rolegroup: rolegroup_ref.clone(), + role_group: rolegroup_name.clone(), })?; cluster_resources .add(client, rg_metrics_service) .await .with_context(|_| ApplyRoleGroupServiceSnafu { - rolegroup: rolegroup_ref.clone(), + role_group: rolegroup_name.clone(), })?; cluster_resources .add(client, rg_configmap) .await .with_context(|_| ApplyRoleGroupConfigSnafu { - rolegroup: rolegroup_ref.clone(), + role_group: rolegroup_name.clone(), })?; // Note: The StatefulSet needs to be applied after all ConfigMaps and Secrets it mounts @@ -412,25 +712,26 @@ pub async fn reconcile_kafka( .add(client, rg_statefulset) .await .with_context(|_| ApplyRoleGroupStatefulSetSnafu { - rolegroup: rolegroup_ref.clone(), + role_group: rolegroup_name.clone(), })?, ); } - let role_config = kafka.role_config(&kafka_role); - if let Some(GenericRoleConfig { - pod_disruption_budget: pdb, - }) = role_config + if let Some(role_config) = validated_cluster.role_configs.get(kafka_role) + && let Some(pdb) = build_pdb(&role_config.pdb, &validated_cluster, kafka_role) { - add_pdbs(pdb, kafka, &kafka_role, client, &mut cluster_resources) + cluster_resources + .add(client, pdb) .await - .context(FailedToCreatePdbSnafu)?; + .context(ApplyPdbSnafu)?; } } - let discovery_cm = - build_discovery_configmap(kafka, kafka, &image, &kafka_security, &bootstrap_listeners) - .context(BuildDiscoveryConfigSnafu)?; + let discovery_cm = build::resource::discovery::build_discovery_configmap( + &validated_cluster, + &bootstrap_listeners, + ) + .context(BuildDiscoveryConfigSnafu)?; cluster_resources .add(client, discovery_cm) @@ -467,3 +768,136 @@ pub fn error_policy( _ => Action::requeue(*Duration::from_secs(5)), } } + +#[cfg(test)] +pub(crate) mod test_support { + use stackable_operator::{ + cli::OperatorEnvironmentOptions, + commons::networking::DomainName, + utils::{cluster_info::KubernetesClusterInfo, yaml_from_str_singleton_map}, + }; + + use super::{ValidatedCluster, dereference::DereferencedObjects, validate::validate}; + use crate::crd::{authentication::ResolvedAuthenticationClasses, v1alpha1}; + + pub fn minimal_kafka(yaml: &str) -> v1alpha1::KafkaCluster { + yaml_from_str_singleton_map(yaml).expect("invalid test KafkaCluster YAML") + } + + fn cluster_info() -> KubernetesClusterInfo { + KubernetesClusterInfo { + cluster_domain: DomainName::try_from("cluster.local").expect("valid domain"), + } + } + + fn operator_environment() -> OperatorEnvironmentOptions { + OperatorEnvironmentOptions { + operator_namespace: "stackable-operators".to_owned(), + operator_service_name: "kafka-operator".to_owned(), + image_repository: "oci.example.org".to_owned(), + } + } + + /// Runs the real validate step against a minimal (auth/OPA-free) fixture. + pub fn validated_cluster(kafka: &v1alpha1::KafkaCluster) -> ValidatedCluster { + validate( + kafka, + DereferencedObjects { + authentication_classes: ResolvedAuthenticationClasses::new(Vec::new()), + authorization_config: None, + kubernetes_cluster_info: cluster_info(), + }, + &operator_environment(), + ) + .expect("validate should succeed for the test fixture") + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use super::{ + PodDescriptorsError, + test_support::{minimal_kafka, validated_cluster}, + }; + use crate::crd::role::KafkaRole; + + /// Two broker role groups whose names hash to the same node-id offset must be + /// rejected: a collision would hand two pods the same Kafka `node.id`. `rg865` + /// and `rg1400` are a known colliding pair for the `broker` role (see + /// [`node_id_hash32_offset`](super::node_id_hasher::node_id_hash32_offset)). + #[test] + fn pod_descriptors_rejects_node_id_hash_collision() { + let kafka = minimal_kafka( + r#" + apiVersion: kafka.stackable.tech/v1alpha1 + kind: KafkaCluster + metadata: + name: simple-kafka + namespace: default + uid: 12345678-1234-1234-1234-123456789012 + spec: + image: + productVersion: 3.9.2 + clusterConfig: + zookeeperConfigMapName: xyz + brokers: + roleGroups: + rg865: + replicas: 1 + rg1400: + replicas: 1 + "#, + ); + let validated = validated_cluster(&kafka); + + match validated.pod_descriptors(None) { + Err(PodDescriptorsError::KafkaNodeIdHashCollision { + role, + colliding_role, + .. + }) => { + assert_eq!(role, KafkaRole::Broker); + assert_eq!(colliding_role, KafkaRole::Broker); + } + other => panic!("expected a node-id hash collision error, got {other:?}"), + } + } + + /// Non-colliding role groups expand to one descriptor per replica, each with a + /// unique `node_id`. + #[test] + fn pod_descriptors_assigns_unique_node_ids() { + let kafka = minimal_kafka( + r#" + apiVersion: kafka.stackable.tech/v1alpha1 + kind: KafkaCluster + metadata: + name: simple-kafka + namespace: default + uid: 12345678-1234-1234-1234-123456789012 + spec: + image: + productVersion: 3.9.2 + clusterConfig: + zookeeperConfigMapName: xyz + brokers: + roleGroups: + default: + replicas: 2 + other: + replicas: 1 + "#, + ); + let validated = validated_cluster(&kafka); + + let descriptors = validated + .pod_descriptors(None) + .expect("non-colliding role groups must not error"); + + assert_eq!(descriptors.len(), 3); + 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:?}"); + } +} diff --git a/rust/operator-binary/src/config/command.rs b/rust/operator-binary/src/controller/build/command.rs similarity index 79% rename from rust/operator-binary/src/config/command.rs rename to rust/operator-binary/src/controller/build/command.rs index a4540001..4805a623 100644 --- a/rust/operator-binary/src/config/command.rs +++ b/rust/operator-binary/src/controller/build/command.rs @@ -4,22 +4,44 @@ use stackable_operator::{ create_vector_shutdown_file_command, remove_vector_shutdown_file_command, }, utils::COMMON_BASH_TRAP_FUNCTIONS, + v2::product_logging::framework::STACKABLE_LOG_DIR, }; +use super::properties::ConfigFileName; use crate::{ + controller::{build::security::copy_opa_tls_cert_command, security::ValidatedKafkaSecurity}, crd::{ - KafkaPodDescriptor, STACKABLE_CONFIG_DIR, STACKABLE_KERBEROS_KRB5_PATH, - role::{broker::BROKER_PROPERTIES_FILE, controller::CONTROLLER_PROPERTIES_FILE}, - security::KafkaTlsSecurity, + BROKER_ID_POD_MAP_DIR, KafkaPodDescriptor, STACKABLE_CONFIG_DIR, + STACKABLE_KERBEROS_KRB5_PATH, STACKABLE_LOG_CONFIG_DIR, }, - product_logging::{BROKER_ID_POD_MAP_DIR, STACKABLE_LOG_DIR}, }; +/// The JVM options selecting the Kafka log4j/log4j2 config file. Kafka 3.x uses log4j, +/// Kafka 4.0 and higher use log4j2. +pub fn kafka_log_opts(product_version: &str) -> String { + if super::properties::uses_legacy_log4j(product_version) { + format!( + "-Dlog4j.configuration=file:{STACKABLE_LOG_CONFIG_DIR}/{log4j}", + log4j = ConfigFileName::Log4j + ) + } else { + format!( + "-Dlog4j2.configurationFile=file:{STACKABLE_LOG_CONFIG_DIR}/{log4j2}", + log4j2 = ConfigFileName::Log4j2 + ) + } +} + +/// The env var carrying the Kafka log4j options (see [`kafka_log_opts`]). +pub fn kafka_log_opts_env_var() -> String { + "KAFKA_LOG4J_OPTS".to_string() +} + /// Returns the commands to start the main Kafka container pub fn broker_kafka_container_commands( kraft_mode: bool, controller_descriptors: Vec, - kafka_security: &KafkaTlsSecurity, + kafka_security: &ValidatedKafkaSecurity, product_version: &str, ) -> String { formatdoc! {" @@ -42,7 +64,7 @@ pub fn broker_kafka_container_commands( true => format!("export KERBEROS_REALM=$(grep -oP 'default_realm = \\K.*' {STACKABLE_KERBEROS_KRB5_PATH})"), false => "".to_string(), }, - import_opa_tls_cert = kafka_security.copy_opa_tls_cert_command(), + import_opa_tls_cert = copy_opa_tls_cert_command(kafka_security), broker_start_command = broker_start_command(kraft_mode, controller_descriptors, product_version), } } @@ -63,12 +85,13 @@ fn broker_start_command( cp {config_dir}/{properties_file} /tmp/{properties_file} config-utils template /tmp/{properties_file} - cp {config_dir}/jaas.properties /tmp/jaas.properties - config-utils template /tmp/jaas.properties + cp {config_dir}/{jaas_file} /tmp/{jaas_file} + config-utils template /tmp/{jaas_file} ", broker_id_pod_map_dir = BROKER_ID_POD_MAP_DIR, config_dir = STACKABLE_CONFIG_DIR, - properties_file = BROKER_PROPERTIES_FILE, + properties_file = ConfigFileName::BrokerProperties, + jaas_file = ConfigFileName::Jaas, }; if kraft_mode { @@ -78,7 +101,7 @@ fn broker_start_command( bin/kafka-storage.sh format --cluster-id \"$KAFKA_CLUSTER_ID\" --config /tmp/{properties_file} --ignore-formatted {initial_controller_command} bin/kafka-server-start.sh /tmp/{properties_file} & ", - properties_file = BROKER_PROPERTIES_FILE, + properties_file = ConfigFileName::BrokerProperties, initial_controller_command = initial_controllers_command(&controller_descriptors, product_version), } } else { @@ -86,7 +109,7 @@ fn broker_start_command( {common_command} bin/kafka-server-start.sh /tmp/{properties_file} &", - properties_file = BROKER_PROPERTIES_FILE, + properties_file = ConfigFileName::BrokerProperties, } } } @@ -158,7 +181,7 @@ pub fn controller_kafka_container_command( ", remove_vector_shutdown_file_command = remove_vector_shutdown_file_command(STACKABLE_LOG_DIR), config_dir = STACKABLE_CONFIG_DIR, - properties_file = CONTROLLER_PROPERTIES_FILE, + properties_file = ConfigFileName::ControllerProperties, initial_controller_command = initial_controllers_command(&controller_descriptors, product_version), create_vector_shutdown_file_command = create_vector_shutdown_file_command(STACKABLE_LOG_DIR) } diff --git a/rust/operator-binary/src/operations/graceful_shutdown.rs b/rust/operator-binary/src/controller/build/graceful_shutdown.rs similarity index 100% rename from rust/operator-binary/src/operations/graceful_shutdown.rs rename to rust/operator-binary/src/controller/build/graceful_shutdown.rs diff --git a/rust/operator-binary/src/config/jvm.rs b/rust/operator-binary/src/controller/build/jvm.rs similarity index 54% rename from rust/operator-binary/src/config/jvm.rs rename to rust/operator-binary/src/controller/build/jvm.rs index 79023618..0a26c38c 100644 --- a/rust/operator-binary/src/config/jvm.rs +++ b/rust/operator-binary/src/controller/build/jvm.rs @@ -1,14 +1,11 @@ -use serde::Serialize; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ memory::{BinaryMultiple, MemoryQuantity}, - role_utils::{self, GenericRoleConfig, JavaCommonConfig, JvmArgumentOverrides, Role}, - schemars::JsonSchema, + v2::jvm_argument_overrides::JvmArgumentOverrides, }; -use crate::crd::{ - JVM_SECURITY_PROPERTIES_FILE, METRICS_PORT, STACKABLE_CONFIG_DIR, role::AnyConfig, -}; +use super::properties::ConfigFileName; +use crate::crd::{METRICS_PORT, STACKABLE_CONFIG_DIR, role::AnyConfig}; const JAVA_HEAP_FACTOR: f32 = 0.8; @@ -21,20 +18,14 @@ pub enum Error { InvalidMemoryConfig { source: stackable_operator::memory::Error, }, - - #[snafu(display("failed to merge jvm argument overrides"))] - MergeJvmArgumentOverrides { source: role_utils::Error }, } -/// All JVM arguments. -fn construct_jvm_args( +/// All JVM arguments: operator-generated base args with the already-merged +/// (role <- role group) `jvmArgumentOverrides` applied on top. +fn construct_jvm_args( merged_config: &AnyConfig, - role: &Role, - role_group: &str, -) -> Result, Error> -where - ConfigOverrides: Default + JsonSchema + Serialize, -{ + jvm_argument_overrides: &JvmArgumentOverrides, +) -> Result, Error> { let heap_size = MemoryQuantity::try_from( merged_config .resources() @@ -51,52 +42,37 @@ where .context(InvalidMemoryConfigSnafu)?; let jvm_args = vec![ - // Heap settings format!("-Xmx{java_heap}"), format!("-Xms{java_heap}"), - format!("-Djava.security.properties={STACKABLE_CONFIG_DIR}/{JVM_SECURITY_PROPERTIES_FILE}"), + format!( + "-Djava.security.properties={STACKABLE_CONFIG_DIR}/{security}", + security = ConfigFileName::Security + ), format!( "-javaagent:/stackable/jmx/jmx_prometheus_javaagent.jar={METRICS_PORT}:/stackable/jmx/server.yaml" ), ]; - let operator_generated = JvmArgumentOverrides::new_with_only_additions(jvm_args); - let merged = role - .get_merged_jvm_argument_overrides(role_group, &operator_generated) - .context(MergeJvmArgumentOverridesSnafu)?; - Ok(merged - .effective_jvm_config_after_merging() - // Sorry for the clone, that's how operator-rs is currently modelled :P - .clone()) + Ok(jvm_argument_overrides.apply_to(jvm_args)) } -/// Arguments that go into `EXTRA_ARGS`, so *not* the heap settings (which you can get using -/// [`construct_heap_jvm_args`]). -pub fn construct_non_heap_jvm_args( +/// Arguments that go into `EXTRA_ARGS` (everything except heap settings). +pub fn construct_non_heap_jvm_args( merged_config: &AnyConfig, - role: &Role, - role_group: &str, -) -> Result -where - ConfigOverrides: Default + JsonSchema + Serialize, -{ - let mut jvm_args = construct_jvm_args(merged_config, role, role_group)?; + jvm_argument_overrides: &JvmArgumentOverrides, +) -> Result { + let mut jvm_args = construct_jvm_args(merged_config, jvm_argument_overrides)?; jvm_args.retain(|arg| !is_heap_jvm_argument(arg)); Ok(jvm_args.join(" ")) } -/// Arguments that go into `KAFKA_HEAP_OPTS`. -/// You can get the normal JVM arguments using [`construct_non_heap_jvm_args`]. -pub fn construct_heap_jvm_args( +/// Arguments that go into `KAFKA_HEAP_OPTS` (only the heap settings). +pub fn construct_heap_jvm_args( merged_config: &AnyConfig, - role: &Role, - role_group: &str, -) -> Result -where - ConfigOverrides: Default + JsonSchema + Serialize, -{ - let mut jvm_args = construct_jvm_args(merged_config, role, role_group)?; + jvm_argument_overrides: &JvmArgumentOverrides, +) -> Result { + let mut jvm_args = construct_jvm_args(merged_config, jvm_argument_overrides)?; jvm_args.retain(|arg| is_heap_jvm_argument(arg)); Ok(jvm_args.join(" ")) @@ -111,7 +87,28 @@ fn is_heap_jvm_argument(jvm_argument: &str) -> bool { #[cfg(test)] mod tests { use super::*; - use crate::crd::{BrokerRole, role::KafkaRole, v1alpha1}; + use crate::{ + controller::test_support::{minimal_kafka, validated_cluster}, + crd::role::KafkaRole, + }; + + /// Pulls the broker `default` role group's merged config + merged JVM overrides out of + /// a validated cluster built from the given YAML. + fn broker_default(yaml: &str) -> (AnyConfig, JvmArgumentOverrides) { + let kafka = minimal_kafka(yaml); + let validated = validated_cluster(&kafka); + let rg = validated + .role_group_configs + .get(&KafkaRole::Broker) + .and_then(|groups| groups.get(&"default".parse().unwrap())) + .expect("broker default role group should exist"); + ( + rg.config.config.clone(), + rg.product_specific_common_config + .jvm_argument_overrides + .clone(), + ) + } #[test] fn test_construct_jvm_arguments_defaults() { @@ -120,6 +117,8 @@ mod tests { kind: KafkaCluster metadata: name: simple-kafka + namespace: default + uid: 12345678-1234-1234-1234-123456789012 spec: image: productVersion: 3.9.2 @@ -130,10 +129,9 @@ mod tests { default: replicas: 1 "#; - let (kafka_role, role, merged_config) = construct_boilerplate(input); - let non_heap_jvm_args = - construct_non_heap_jvm_args(&kafka_role, &role, &merged_config).unwrap(); - let heap_jvm_args = construct_heap_jvm_args(&kafka_role, &role, &merged_config).unwrap(); + let (merged_config, jvm) = broker_default(input); + let non_heap_jvm_args = construct_non_heap_jvm_args(&merged_config, &jvm).unwrap(); + let heap_jvm_args = construct_heap_jvm_args(&merged_config, &jvm).unwrap(); assert_eq!( non_heap_jvm_args, @@ -150,6 +148,8 @@ mod tests { kind: KafkaCluster metadata: name: simple-kafka + namespace: default + uid: 12345678-1234-1234-1234-123456789012 spec: image: productVersion: 3.9.2 @@ -177,10 +177,9 @@ mod tests { - -Xmx40000m - -Dhttps.proxyPort=1234 "#; - let (merged_config, role, role_group) = construct_boilerplate(input); - let non_heap_jvm_args = - construct_non_heap_jvm_args(&merged_config, &role, &role_group).unwrap(); - let heap_jvm_args = construct_heap_jvm_args(&merged_config, &role, &role_group).unwrap(); + let (merged_config, jvm) = broker_default(input); + let non_heap_jvm_args = construct_non_heap_jvm_args(&merged_config, &jvm).unwrap(); + let heap_jvm_args = construct_heap_jvm_args(&merged_config, &jvm).unwrap(); assert_eq!( non_heap_jvm_args, @@ -192,18 +191,4 @@ mod tests { ); assert_eq!(heap_jvm_args, "-Xms34406m -Xmx40000m"); } - - fn construct_boilerplate(kafka_cluster: &str) -> (AnyConfig, BrokerRole, String) { - let kafka: v1alpha1::KafkaCluster = - serde_yaml::from_str(kafka_cluster).expect("illegal test input"); - - let kafka_role = KafkaRole::Broker; - let rolegroup_ref = kafka.rolegroup_ref(&kafka_role, "default"); - let merged_config = kafka_role - .merged_config(&kafka, &rolegroup_ref.role_group) - .unwrap(); - let role = kafka.spec.brokers.unwrap(); - - (merged_config, role, "default".to_owned()) - } } diff --git a/rust/operator-binary/src/kerberos.rs b/rust/operator-binary/src/controller/build/kerberos.rs similarity index 89% rename from rust/operator-binary/src/kerberos.rs rename to rust/operator-binary/src/controller/build/kerberos.rs index 971cee5c..676c45b0 100644 --- a/rust/operator-binary/src/kerberos.rs +++ b/rust/operator-binary/src/controller/build/kerberos.rs @@ -14,9 +14,12 @@ use stackable_operator::{ commons::secret_class::SecretClassVolumeProvisionParts, }; -use crate::crd::{ - LISTENER_BOOTSTRAP_VOLUME_NAME, LISTENER_BROKER_VOLUME_NAME, STACKABLE_KERBEROS_DIR, - STACKABLE_KERBEROS_KRB5_PATH, role::KafkaRole, security::KafkaTlsSecurity, +use crate::{ + controller::security::ValidatedKafkaSecurity, + crd::{ + LISTENER_BOOTSTRAP_VOLUME_NAME, LISTENER_BROKER_VOLUME_NAME, STACKABLE_KERBEROS_DIR, + STACKABLE_KERBEROS_KRB5_PATH, role::KafkaRole, + }, }; #[derive(Snafu, Debug)] @@ -36,7 +39,7 @@ pub enum Error { } pub fn add_kerberos_pod_config( - kafka_security: &KafkaTlsSecurity, + kafka_security: &ValidatedKafkaSecurity, role: &KafkaRole, cb_kcat_prober: &mut ContainerBuilder, cb_kafka: &mut ContainerBuilder, diff --git a/rust/operator-binary/src/controller/build/mod.rs b/rust/operator-binary/src/controller/build/mod.rs new file mode 100644 index 00000000..5e837150 --- /dev/null +++ b/rust/operator-binary/src/controller/build/mod.rs @@ -0,0 +1,9 @@ +//! Builders that assemble Kubernetes resources for kafka rolegroups. + +pub mod command; +pub mod graceful_shutdown; +pub mod jvm; +pub mod kerberos; +pub mod properties; +pub mod resource; +pub mod security; diff --git a/rust/operator-binary/src/controller/build/properties/broker_properties.rs b/rust/operator-binary/src/controller/build/properties/broker_properties.rs new file mode 100644 index 00000000..ec7d6957 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/broker_properties.rs @@ -0,0 +1,118 @@ +use std::collections::BTreeMap; + +use super::kraft_controllers; +use crate::{ + controller::{ + ValidatedClusterConfig, + build::{ + graceful_shutdown::graceful_shutdown_config_properties, + security::broker_config_settings, + }, + }, + crd::{ + KafkaPodDescriptor, + listener::{KafkaListenerConfig, KafkaListenerName}, + role::{ + KAFKA_ADVERTISED_LISTENERS, KAFKA_BROKER_ID, KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS, + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP, KAFKA_LISTENERS, KAFKA_LOG_DIRS, KAFKA_NODE_ID, + KAFKA_PROCESS_ROLES, KafkaRole, + }, + }, +}; + +pub fn build( + cluster_config: &ValidatedClusterConfig, + listener_config: &KafkaListenerConfig, + pod_descriptors: &[KafkaPodDescriptor], + overrides: BTreeMap, +) -> BTreeMap { + let kraft_controllers = kraft_controllers(pod_descriptors); + + let mut result = BTreeMap::from([ + ( + KAFKA_LOG_DIRS.to_string(), + "/stackable/data/topicdata".to_string(), + ), + (KAFKA_LISTENERS.to_string(), listener_config.listeners()), + ( + KAFKA_ADVERTISED_LISTENERS.to_string(), + listener_config.advertised_listeners(), + ), + ( + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP.to_string(), + listener_config.listener_security_protocol_map(), + ), + ( + "inter.broker.listener.name".to_string(), + KafkaListenerName::Internal.to_string(), + ), + ]); + + if cluster_config.is_kraft_mode() { + let kraft_controllers = kraft_controllers.join(","); + + // Running in KRaft mode + result.extend([ + ( + "broker.id.generation.enable".to_string(), + "false".to_string(), + ), + (KAFKA_NODE_ID.to_string(), "${env:REPLICA_ID}".to_string()), + ( + KAFKA_PROCESS_ROLES.to_string(), + KafkaRole::Broker.to_string(), + ), + ( + "controller.listener.names".to_string(), + KafkaListenerName::Controller.to_string(), + ), + ( + KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS.to_string(), + kraft_controllers.clone(), + ), + ]); + } else { + // Running with ZooKeeper enabled + result.extend([( + "zookeeper.connect".to_string(), + "${env:ZOOKEEPER}".to_string(), + )]); + // We are in zookeeper mode and the user has defined a broker id mapping + // so we disable automatic id generation. + // This check ensures that existing clusters running in ZooKeeper mode do not + // suddenly break after the introduction of this change. + if cluster_config.disable_broker_id_generation() { + result.extend([ + ( + "broker.id.generation.enable".to_string(), + "false".to_string(), + ), + (KAFKA_BROKER_ID.to_string(), "${env:REPLICA_ID}".to_string()), + ]); + } + } + + // Enable OPA authorization + if let Some(opa_connect_string) = cluster_config.opa_connect() { + result.extend([ + ( + "authorizer.class.name".to_string(), + "org.openpolicyagent.kafka.OpaAuthorizer".to_string(), + ), + ( + "opa.authorizer.metrics.enabled".to_string(), + "true".to_string(), + ), + ( + "opa.authorizer.url".to_string(), + opa_connect_string.to_string(), + ), + ]); + } + + result.extend(broker_config_settings(&cluster_config.kafka_security)); + result.extend(graceful_shutdown_config_properties()); + result.extend(overrides); + + result +} diff --git a/rust/operator-binary/src/controller/build/properties/controller_properties.rs b/rust/operator-binary/src/controller/build/properties/controller_properties.rs new file mode 100644 index 00000000..4044e4f6 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/controller_properties.rs @@ -0,0 +1,77 @@ +use std::collections::BTreeMap; + +use super::kraft_controllers; +use crate::{ + controller::{ + ValidatedClusterConfig, + build::{ + graceful_shutdown::graceful_shutdown_config_properties, + security::controller_config_settings, + }, + }, + crd::{ + KafkaPodDescriptor, + listener::{KafkaListenerConfig, KafkaListenerName}, + role::{ + KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS, KAFKA_LISTENER_SECURITY_PROTOCOL_MAP, + KAFKA_LISTENERS, KAFKA_LOG_DIRS, KAFKA_NODE_ID, KAFKA_PROCESS_ROLES, KafkaRole, + }, + }, +}; + +pub fn build( + cluster_config: &ValidatedClusterConfig, + listener_config: &KafkaListenerConfig, + pod_descriptors: &[KafkaPodDescriptor], + overrides: BTreeMap, +) -> BTreeMap { + let kraft_controllers = kraft_controllers(pod_descriptors).join(","); + + let mut result = BTreeMap::from([ + ( + KAFKA_LOG_DIRS.to_string(), + "/stackable/data/kraft".to_string(), + ), + (KAFKA_PROCESS_ROLES.to_string(), KafkaRole::Controller.to_string()), + ( + "controller.listener.names".to_string(), + KafkaListenerName::Controller.to_string(), + ), + ( + KAFKA_NODE_ID.to_string(), + "${env:REPLICA_ID}".to_string(), + ), + ( + KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS.to_string(), + kraft_controllers.clone(), + ), + ( + KAFKA_LISTENERS.to_string(), + "CONTROLLER://${env:POD_NAME}.${env:ROLEGROUP_HEADLESS_SERVICE_NAME}.${env:NAMESPACE}.svc.${env:CLUSTER_DOMAIN}:${env:KAFKA_CLIENT_PORT}".to_string(), + ), + ( + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP.to_string(), + listener_config + .listener_security_protocol_map_for_controller()), + ]); + + result.insert( + "inter.broker.listener.name".to_string(), + KafkaListenerName::Internal.to_string(), + ); + + // The ZooKeeper connection is needed for migration from ZooKeeper to KRaft mode. + // It is not needed once the controller is fully running in KRaft mode. + if !cluster_config.is_kraft_mode() { + result.insert( + "zookeeper.connect".to_string(), + "${env:ZOOKEEPER}".to_string(), + ); + } + + result.extend(controller_config_settings(&cluster_config.kafka_security)); + result.extend(graceful_shutdown_config_properties()); + result.extend(overrides); + + result +} diff --git a/rust/operator-binary/src/controller/build/properties/listener.rs b/rust/operator-binary/src/controller/build/properties/listener.rs new file mode 100644 index 00000000..ab8bd205 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/listener.rs @@ -0,0 +1,532 @@ +//! Builds the Kafka listener configuration (`listeners` / `advertised.listeners` / +//! `listener.security.protocol.map`) for a role group's broker/controller properties. +//! +//! Consumes the [`ValidatedCluster`] (for the namespace and role-group resource names) instead of +//! the raw CRD. + +use std::collections::BTreeMap; + +use stackable_operator::{ + utils::cluster_info::KubernetesClusterInfo, v2::types::kubernetes::NamespaceName, +}; + +use crate::{ + controller::{RoleGroupName, ValidatedCluster, security::ValidatedKafkaSecurity}, + crd::{ + STACKABLE_LISTENER_BROKER_DIR, + listener::{ + KafkaListener, KafkaListenerConfig, KafkaListenerName, KafkaListenerProtocol, + LISTENER_LOCAL_ADDRESS, node_address_cmd, node_port_cmd, + }, + role::KafkaRole, + }, +}; + +pub fn get_kafka_listener_config( + validated_cluster: &ValidatedCluster, + kafka_security: &ValidatedKafkaSecurity, + role: &KafkaRole, + role_group_name: &RoleGroupName, + cluster_info: &KubernetesClusterInfo, +) -> KafkaListenerConfig { + let headless_service_name = validated_cluster + .resource_names(role, role_group_name) + .headless_service_name(); + let pod_fqdn = pod_fqdn( + &validated_cluster.namespace, + headless_service_name.as_ref(), + cluster_info, + ); + let mut listeners = vec![]; + let mut advertised_listeners = vec![]; + let mut listener_security_protocol_map: BTreeMap = + BTreeMap::new(); + + // CLIENT + if kafka_security.has_kerberos_enabled() { + // 1) Kerberos and TLS authentication classes are mutually exclusive + listeners.push(KafkaListener { + name: KafkaListenerName::Client, + host: LISTENER_LOCAL_ADDRESS.to_string(), + port: ValidatedKafkaSecurity::SECURE_CLIENT_PORT.to_string(), + }); + advertised_listeners.push(KafkaListener { + name: KafkaListenerName::Client, + host: node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), + port: node_port_cmd( + STACKABLE_LISTENER_BROKER_DIR, + kafka_security.client_port_name(), + ), + }); + listener_security_protocol_map + .insert(KafkaListenerName::Client, KafkaListenerProtocol::SaslSsl); + } else if kafka_security.tls_client_authentication_class().is_some() + || kafka_security.tls_server_secret_class().is_some() + { + // 2) Client listener uses TLS (possibly with authentication) + listeners.push(KafkaListener { + name: KafkaListenerName::Client, + host: LISTENER_LOCAL_ADDRESS.to_string(), + port: kafka_security.client_port().to_string(), + }); + advertised_listeners.push(KafkaListener { + name: KafkaListenerName::Client, + host: node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), + port: node_port_cmd( + STACKABLE_LISTENER_BROKER_DIR, + kafka_security.client_port_name(), + ), + }); + listener_security_protocol_map + .insert(KafkaListenerName::Client, KafkaListenerProtocol::Ssl); + } else { + // 3) If no client auth or tls is required we expose CLIENT with PLAINTEXT + listeners.push(KafkaListener { + name: KafkaListenerName::Client, + host: LISTENER_LOCAL_ADDRESS.to_string(), + port: ValidatedKafkaSecurity::CLIENT_PORT.to_string(), + }); + advertised_listeners.push(KafkaListener { + name: KafkaListenerName::Client, + host: node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), + port: node_port_cmd( + STACKABLE_LISTENER_BROKER_DIR, + kafka_security.client_port_name(), + ), + }); + listener_security_protocol_map + .insert(KafkaListenerName::Client, KafkaListenerProtocol::Plaintext); + } + + // INTERNAL / CONTROLLER + if kafka_security.has_kerberos_enabled() || kafka_security.tls_internal_secret_class().is_some() + { + // 5) & 6) Kerberos and TLS authentication classes are mutually exclusive but both require internal tls to be used + listeners.push(KafkaListener { + name: KafkaListenerName::Internal, + host: LISTENER_LOCAL_ADDRESS.to_string(), + port: ValidatedKafkaSecurity::SECURE_INTERNAL_PORT.to_string(), + }); + advertised_listeners.push(KafkaListener { + name: KafkaListenerName::Internal, + host: pod_fqdn.to_string(), + port: ValidatedKafkaSecurity::SECURE_INTERNAL_PORT.to_string(), + }); + listener_security_protocol_map + .insert(KafkaListenerName::Internal, KafkaListenerProtocol::Ssl); + listener_security_protocol_map + .insert(KafkaListenerName::Controller, KafkaListenerProtocol::Ssl); + } else { + // 7) If no internal tls is required we expose INTERNAL as PLAINTEXT + listeners.push(KafkaListener { + name: KafkaListenerName::Internal, + host: LISTENER_LOCAL_ADDRESS.to_string(), + port: kafka_security.internal_port().to_string(), + }); + advertised_listeners.push(KafkaListener { + name: KafkaListenerName::Internal, + host: pod_fqdn.to_string(), + port: kafka_security.internal_port().to_string(), + }); + listener_security_protocol_map.insert( + KafkaListenerName::Internal, + KafkaListenerProtocol::Plaintext, + ); + listener_security_protocol_map.insert( + KafkaListenerName::Controller, + KafkaListenerProtocol::Plaintext, + ); + } + + // BOOTSTRAP + if kafka_security.has_kerberos_enabled() { + listeners.push(KafkaListener { + name: KafkaListenerName::Bootstrap, + host: LISTENER_LOCAL_ADDRESS.to_string(), + port: kafka_security.bootstrap_port().to_string(), + }); + advertised_listeners.push(KafkaListener { + name: KafkaListenerName::Bootstrap, + host: node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), + port: node_port_cmd( + STACKABLE_LISTENER_BROKER_DIR, + kafka_security.client_port_name(), + ), + }); + listener_security_protocol_map + .insert(KafkaListenerName::Bootstrap, KafkaListenerProtocol::SaslSsl); + } + + KafkaListenerConfig { + listeners, + advertised_listeners, + listener_security_protocol_map, + } +} + +pub(crate) fn pod_fqdn( + namespace: &NamespaceName, + sts_service_name: &str, + cluster_info: &KubernetesClusterInfo, +) -> String { + format!( + "${{env:POD_NAME}}.{sts_service_name}.{namespace}.svc.{cluster_domain}", + cluster_domain = cluster_info.cluster_domain + ) +} + +#[cfg(test)] +mod tests { + use stackable_operator::{ + builder::meta::ObjectMetaBuilder, + commons::networking::DomainName, + crd::authentication::{core, kerberos, tls}, + }; + + use super::*; + use crate::{ + controller::test_support::{minimal_kafka, validated_cluster}, + crd::authentication::ResolvedAuthenticationClasses, + }; + + fn default_cluster_info() -> KubernetesClusterInfo { + KubernetesClusterInfo { + cluster_domain: DomainName::try_from("cluster.local").unwrap(), + } + } + + #[test] + fn test_get_kafka_listeners_config() { + // The fixture only needs to resolve a `ValidatedCluster` (for the namespace and the + // `--` resource names); the listener output is driven by the + // explicit `kafka_security` below, so no authentication/TLS is configured here. + let kafka_cluster = r#" + apiVersion: kafka.stackable.tech/v1alpha1 + kind: KafkaCluster + metadata: + name: simple-kafka + namespace: default + uid: 12345678-1234-1234-1234-123456789012 + spec: + image: + productVersion: 3.9.2 + clusterConfig: + zookeeperConfigMapName: xyz + brokers: + roleGroups: + default: + replicas: 1 + "#; + let kafka = minimal_kafka(kafka_cluster); + let validated = validated_cluster(&kafka); + let kafka_security = ValidatedKafkaSecurity::new( + ResolvedAuthenticationClasses::new(vec![core::v1alpha1::AuthenticationClass { + metadata: ObjectMetaBuilder::new().name("auth-class").build(), + spec: core::v1alpha1::AuthenticationClassSpec { + provider: core::v1alpha1::AuthenticationClassProvider::Tls( + tls::v1alpha1::AuthenticationProvider { + client_cert_secret_class: Some("client-auth-secret-class".to_string()), + }, + ), + }, + }]), + Some("internal-tls".parse().unwrap()), + Some("tls".parse().unwrap()), + None, + ); + let cluster_info = default_cluster_info(); + let role_group_name: RoleGroupName = "default".parse().unwrap(); + let config = get_kafka_listener_config( + &validated, + &kafka_security, + &KafkaRole::Broker, + &role_group_name, + &cluster_info, + ); + + assert_eq!( + config.listeners(), + format!( + "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", + name = KafkaListenerName::Client, + host = LISTENER_LOCAL_ADDRESS, + port = kafka_security.client_port(), + internal_name = KafkaListenerName::Internal, + internal_host = LISTENER_LOCAL_ADDRESS, + internal_port = kafka_security.internal_port(), + ) + ); + + assert_eq!( + config.advertised_listeners(), + format!( + "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", + name = KafkaListenerName::Client, + host = node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), + port = node_port_cmd( + STACKABLE_LISTENER_BROKER_DIR, + kafka_security.client_port_name() + ), + internal_name = KafkaListenerName::Internal, + internal_host = pod_fqdn( + &validated.namespace, + validated + .resource_names(&KafkaRole::Broker, &role_group_name) + .headless_service_name() + .as_ref(), + &cluster_info + ), + internal_port = kafka_security.internal_port(), + ) + ); + + assert_eq!( + config.listener_security_protocol_map(), + format!( + "{name}:{protocol},{internal_name}:{internal_protocol},{controller_name}:{controller_protocol}", + name = KafkaListenerName::Client, + protocol = KafkaListenerProtocol::Ssl, + internal_name = KafkaListenerName::Internal, + internal_protocol = KafkaListenerProtocol::Ssl, + controller_name = KafkaListenerName::Controller, + controller_protocol = KafkaListenerProtocol::Ssl, + ) + ); + + let kafka_security = ValidatedKafkaSecurity::new( + ResolvedAuthenticationClasses::new(vec![]), + Some("tls".parse().unwrap()), + Some("tls".parse().unwrap()), + None, + ); + let config = get_kafka_listener_config( + &validated, + &kafka_security, + &KafkaRole::Broker, + &role_group_name, + &cluster_info, + ); + + assert_eq!( + config.listeners(), + format!( + "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", + name = KafkaListenerName::Client, + host = LISTENER_LOCAL_ADDRESS, + port = kafka_security.client_port(), + internal_name = KafkaListenerName::Internal, + internal_host = LISTENER_LOCAL_ADDRESS, + internal_port = kafka_security.internal_port(), + ) + ); + + assert_eq!( + config.advertised_listeners(), + format!( + "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", + name = KafkaListenerName::Client, + host = node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), + port = node_port_cmd( + STACKABLE_LISTENER_BROKER_DIR, + kafka_security.client_port_name() + ), + internal_name = KafkaListenerName::Internal, + internal_host = pod_fqdn( + &validated.namespace, + validated + .resource_names(&KafkaRole::Broker, &role_group_name) + .headless_service_name() + .as_ref(), + &cluster_info + ), + internal_port = kafka_security.internal_port(), + ) + ); + + assert_eq!( + config.listener_security_protocol_map(), + format!( + "{name}:{protocol},{internal_name}:{internal_protocol},{controller_name}:{controller_protocol}", + name = KafkaListenerName::Client, + protocol = KafkaListenerProtocol::Ssl, + internal_name = KafkaListenerName::Internal, + internal_protocol = KafkaListenerProtocol::Ssl, + controller_name = KafkaListenerName::Controller, + controller_protocol = KafkaListenerProtocol::Ssl, + ) + ); + + let kafka_security = ValidatedKafkaSecurity::new( + ResolvedAuthenticationClasses::new(vec![]), + None, + None, + None, + ); + + let config = get_kafka_listener_config( + &validated, + &kafka_security, + &KafkaRole::Broker, + &role_group_name, + &cluster_info, + ); + + assert_eq!( + config.listeners(), + format!( + "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", + name = KafkaListenerName::Client, + host = LISTENER_LOCAL_ADDRESS, + port = kafka_security.client_port(), + internal_name = KafkaListenerName::Internal, + internal_host = LISTENER_LOCAL_ADDRESS, + internal_port = kafka_security.internal_port(), + ) + ); + + assert_eq!( + config.advertised_listeners(), + format!( + "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", + name = KafkaListenerName::Client, + host = node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), + port = node_port_cmd( + STACKABLE_LISTENER_BROKER_DIR, + kafka_security.client_port_name() + ), + internal_name = KafkaListenerName::Internal, + internal_host = pod_fqdn( + &validated.namespace, + validated + .resource_names(&KafkaRole::Broker, &role_group_name) + .headless_service_name() + .as_ref(), + &cluster_info + ), + internal_port = kafka_security.internal_port(), + ) + ); + + assert_eq!( + config.listener_security_protocol_map(), + format!( + "{name}:{protocol},{internal_name}:{internal_protocol},{controller_name}:{controller_protocol}", + name = KafkaListenerName::Client, + protocol = KafkaListenerProtocol::Plaintext, + internal_name = KafkaListenerName::Internal, + internal_protocol = KafkaListenerProtocol::Plaintext, + controller_name = KafkaListenerName::Controller, + controller_protocol = KafkaListenerProtocol::Plaintext, + ) + ); + } + + #[test] + fn test_get_kafka_kerberos_listeners_config() { + // See the comment in `test_get_kafka_listeners_config`: the fixture only resolves a + // `ValidatedCluster`; Kerberos is configured via the explicit `kafka_security` below. + let kafka_cluster = r#" + apiVersion: kafka.stackable.tech/v1alpha1 + kind: KafkaCluster + metadata: + name: simple-kafka + namespace: default + uid: 12345678-1234-1234-1234-123456789012 + spec: + image: + productVersion: 3.9.2 + clusterConfig: + zookeeperConfigMapName: xyz + brokers: + roleGroups: + default: + replicas: 1 + "#; + let kafka = minimal_kafka(kafka_cluster); + let validated = validated_cluster(&kafka); + let kafka_security = ValidatedKafkaSecurity::new( + ResolvedAuthenticationClasses::new(vec![core::v1alpha1::AuthenticationClass { + metadata: ObjectMetaBuilder::new().name("auth-class").build(), + spec: core::v1alpha1::AuthenticationClassSpec { + provider: core::v1alpha1::AuthenticationClassProvider::Kerberos( + kerberos::v1alpha1::AuthenticationProvider { + kerberos_secret_class: "kerberos-secret-class".to_string(), + }, + ), + }, + }]), + Some("tls".parse().unwrap()), + Some("tls".parse().unwrap()), + None, + ); + let cluster_info = default_cluster_info(); + let role_group_name: RoleGroupName = "default".parse().unwrap(); + let config = get_kafka_listener_config( + &validated, + &kafka_security, + &KafkaRole::Broker, + &role_group_name, + &cluster_info, + ); + + assert_eq!( + config.listeners(), + format!( + "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port},{bootstrap_name}://{bootstrap_host}:{bootstrap_port}", + name = KafkaListenerName::Client, + host = LISTENER_LOCAL_ADDRESS, + port = kafka_security.client_port(), + internal_name = KafkaListenerName::Internal, + internal_host = LISTENER_LOCAL_ADDRESS, + internal_port = kafka_security.internal_port(), + bootstrap_name = KafkaListenerName::Bootstrap, + bootstrap_host = LISTENER_LOCAL_ADDRESS, + bootstrap_port = kafka_security.bootstrap_port(), + ) + ); + + assert_eq!( + config.advertised_listeners(), + format!( + "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port},{bootstrap_name}://{bootstrap_host}:{bootstrap_port}", + name = KafkaListenerName::Client, + host = node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), + port = node_port_cmd( + STACKABLE_LISTENER_BROKER_DIR, + kafka_security.client_port_name() + ), + internal_name = KafkaListenerName::Internal, + internal_host = pod_fqdn( + &validated.namespace, + validated + .resource_names(&KafkaRole::Broker, &role_group_name) + .headless_service_name() + .as_ref(), + &cluster_info + ), + internal_port = kafka_security.internal_port(), + bootstrap_name = KafkaListenerName::Bootstrap, + bootstrap_host = node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), + bootstrap_port = node_port_cmd( + STACKABLE_LISTENER_BROKER_DIR, + kafka_security.client_port_name() + ), + ) + ); + + assert_eq!( + config.listener_security_protocol_map(), + format!( + "{name}:{protocol},{internal_name}:{internal_protocol},{bootstrap_name}:{bootstrap_protocol},{controller_name}:{controller_protocol}", + name = KafkaListenerName::Client, + protocol = KafkaListenerProtocol::SaslSsl, + internal_name = KafkaListenerName::Internal, + internal_protocol = KafkaListenerProtocol::Ssl, + bootstrap_name = KafkaListenerName::Bootstrap, + bootstrap_protocol = KafkaListenerProtocol::SaslSsl, + controller_name = KafkaListenerName::Controller, + controller_protocol = KafkaListenerProtocol::Ssl, + ) + ); + } +} diff --git a/rust/operator-binary/src/controller/build/properties/mod.rs b/rust/operator-binary/src/controller/build/properties/mod.rs new file mode 100644 index 00000000..bedf3db8 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/mod.rs @@ -0,0 +1,93 @@ +//! Property-file builders for Kafka rolegroup ConfigMaps. + +pub mod broker_properties; +pub mod controller_properties; +pub mod listener; +pub mod product_logging; +pub mod security_properties; + +use crate::crd::{ + KafkaPodDescriptor, + role::{AnyConfig, KafkaRole}, +}; + +/// The names of the config files assembled into the rolegroup `ConfigMap`. +/// +/// A single source of truth for the on-disk file names, used by the config-map +/// builder, the per-file property builders and the JVM/command builders. +#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display)] +pub enum ConfigFileName { + #[strum(serialize = "broker.properties")] + BrokerProperties, + #[strum(serialize = "controller.properties")] + ControllerProperties, + #[strum(serialize = "security.properties")] + Security, + #[strum(serialize = "client.properties")] + Client, + /// JAAS configuration for Kerberos authentication. It has the `.properties` + /// extension but is not a Java properties file. + #[strum(serialize = "jaas.properties")] + Jaas, + /// Used by Kafka 3.x. + #[strum(serialize = "log4j.properties")] + Log4j, + /// Used by Kafka 4.0 and later. + #[strum(serialize = "log4j2.properties")] + Log4j2, +} + +/// The product config-file name for a role group, derived from its role +/// (`broker.properties` for brokers, `controller.properties` for controllers). +pub fn config_file_name(config: &AnyConfig) -> ConfigFileName { + match config { + AnyConfig::Broker(_) => ConfigFileName::BrokerProperties, + AnyConfig::Controller(_) => ConfigFileName::ControllerProperties, + } +} + +/// Whether the given Kafka version uses the legacy log4j logging framework. +/// +/// Kafka 3.x uses log4j ([`ConfigFileName::Log4j`]); Kafka 4.0 and later use log4j2 +/// ([`ConfigFileName::Log4j2`]). This is the single source of truth for that decision, +/// used both when rendering the log config file and when selecting the JVM option that +/// points at it. +pub fn uses_legacy_log4j(product_version: &str) -> bool { + product_version.starts_with("3.") +} + +pub(crate) fn kraft_controllers(pod_descriptors: &[KafkaPodDescriptor]) -> Vec { + pod_descriptors + .iter() + .filter(|pd| pd.role == KafkaRole::Controller) + .map(|desc| { + format!( + "{fqdn}:{client_port}", + fqdn = desc.fqdn(), + client_port = desc.client_port + ) + }) + .collect::>() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn file_names_match_the_kafka_on_disk_names() { + assert_eq!( + ConfigFileName::BrokerProperties.to_string(), + "broker.properties" + ); + assert_eq!( + ConfigFileName::ControllerProperties.to_string(), + "controller.properties" + ); + assert_eq!(ConfigFileName::Security.to_string(), "security.properties"); + assert_eq!(ConfigFileName::Client.to_string(), "client.properties"); + assert_eq!(ConfigFileName::Jaas.to_string(), "jaas.properties"); + assert_eq!(ConfigFileName::Log4j.to_string(), "log4j.properties"); + assert_eq!(ConfigFileName::Log4j2.to_string(), "log4j2.properties"); + } +} diff --git a/rust/operator-binary/src/controller/build/properties/product_logging/mod.rs b/rust/operator-binary/src/controller/build/properties/product_logging/mod.rs new file mode 100644 index 00000000..e643b197 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/product_logging/mod.rs @@ -0,0 +1,152 @@ +//! Renders the logging config files (`log4j.properties` / `log4j2.properties` and the +//! Vector agent config) assembled into the rolegroup `ConfigMap`. + +use std::{borrow::Cow, collections::BTreeMap, fmt::Display}; + +use stackable_operator::{ + memory::{BinaryMultiple, MemoryQuantity}, + product_logging::{ + self, + spec::{ContainerLogConfig, ContainerLogConfigChoice}, + }, + v2::product_logging::framework::STACKABLE_LOG_DIR, +}; + +use super::ConfigFileName; +use crate::crd::role::{AnyConfig, broker::BrokerContainer, controller::ControllerContainer}; + +/// The maximum size of a single Kafka log file before it is rotated. +pub const MAX_KAFKA_LOG_FILES_SIZE: MemoryQuantity = MemoryQuantity { + value: 10.0, + unit: BinaryMultiple::Mebi, +}; + +const KAFKA_LOG4J_FILE: &str = "kafka.log4j.xml"; +const KAFKA_LOG4J2_FILE: &str = "kafka.log4j2.xml"; + +/// The static, env-driven Vector agent configuration (`vector.yaml`). +/// +/// The [`vector_container`](stackable_operator::v2::product_logging::framework::vector_container) +/// mounts this file and supplies its `${...}` values (`LOG_DIR`, `DATA_DIR`, `NAMESPACE`, +/// `CLUSTER_NAME`, `ROLE_NAME`, `ROLE_GROUP_NAME`, `VECTOR_AGGREGATOR_ADDRESS`) as env vars. +const VECTOR_CONFIG: &str = include_str!("vector.yaml"); + +/// Returns the Vector agent config (`vector.yaml`) content. +pub fn vector_config_file_content() -> String { + VECTOR_CONFIG.to_owned() +} + +const CONSOLE_CONVERSION_PATTERN_LOG4J: &str = "[%d] %p %m (%c)%n"; +const CONSOLE_CONVERSION_PATTERN_LOG4J2: &str = "%d{ISO8601} %p [%t] %c - %m%n"; + +/// Get the role group ConfigMap data with the log4j/log4j2 logging configuration. +/// +/// The Vector agent config (`vector.yaml`) is provided separately via +/// [`vector_config_file_content`] and added by the caller. +pub fn role_group_config_map_data( + product_version: &str, + merged_config: &AnyConfig, +) -> BTreeMap> { + let container_name = match merged_config { + AnyConfig::Broker(_) => BrokerContainer::Kafka.to_string(), + AnyConfig::Controller(_) => ControllerContainer::Kafka.to_string(), + }; + + let mut configs: BTreeMap> = BTreeMap::new(); + + // Starting with Kafka 4.0, log4j2 is used instead of log4j. + match super::uses_legacy_log4j(product_version) { + true => { + configs.insert( + ConfigFileName::Log4j.to_string(), + log4j_config_if_automatic( + Some(merged_config.kafka_logging()), + container_name, + KAFKA_LOG4J_FILE, + MAX_KAFKA_LOG_FILES_SIZE, + ), + ); + } + false => { + configs.insert( + ConfigFileName::Log4j2.to_string(), + log4j2_config_if_automatic( + Some(merged_config.kafka_logging()), + container_name, + KAFKA_LOG4J2_FILE, + MAX_KAFKA_LOG_FILES_SIZE, + ), + ); + } + } + + configs +} + +fn log4j_config_if_automatic( + log_config: Option>, + container_name: impl Display, + log_file: &str, + max_log_file_size: MemoryQuantity, +) -> Option { + if let Some(ContainerLogConfig { + choice: Some(ContainerLogConfigChoice::Automatic(log_config)), + }) = log_config.as_deref() + { + Some(product_logging::framework::create_log4j_config( + &format!("{STACKABLE_LOG_DIR}/{container_name}"), + log_file, + max_log_file_size + .scale_to(BinaryMultiple::Mebi) + .floor() + .value as u32, + CONSOLE_CONVERSION_PATTERN_LOG4J, + log_config, + )) + } else { + None + } +} + +fn log4j2_config_if_automatic( + log_config: Option>, + container_name: impl Display, + log_file: &str, + max_log_file_size: MemoryQuantity, +) -> Option { + if let Some(ContainerLogConfig { + choice: Some(ContainerLogConfigChoice::Automatic(log_config)), + }) = log_config.as_deref() + { + Some(product_logging::framework::create_log4j2_config( + &format!("{STACKABLE_LOG_DIR}/{container_name}",), + log_file, + max_log_file_size + .scale_to(BinaryMultiple::Mebi) + .floor() + .value as u32, + CONSOLE_CONVERSION_PATTERN_LOG4J2, + log_config, + )) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_vector_config_file_content() { + let content = vector_config_file_content(); + assert!(!content.is_empty()); + // The two Kafka log formats must be handled ... + assert!(content.contains("files_log4j")); + assert!(content.contains("files_log4j2")); + // ... while the non-Kafka sources were removed. + assert!(!content.contains("files_stdout")); + assert!(!content.contains("files_tracing_rs")); + assert!(!content.contains("files_opa_json")); + } +} diff --git a/rust/operator-binary/src/controller/build/properties/product_logging/test-vector.sh b/rust/operator-binary/src/controller/build/properties/product_logging/test-vector.sh new file mode 100755 index 00000000..d7535667 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/product_logging/test-vector.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env sh + +DATA_DIR=/stackable/log/_vector-state \ +LOG_DIR=/stackable/log \ +NAMESPACE=default \ +CLUSTER_NAME=kafka \ +ROLE_NAME=broker \ +ROLE_GROUP_NAME=default \ +VECTOR_AGGREGATOR_ADDRESS=vector-aggregator \ +VECTOR_FILE_LOG_LEVEL=info \ +vector test vector.yaml vector-test.yaml diff --git a/rust/operator-binary/src/controller/build/properties/product_logging/vector-test.yaml b/rust/operator-binary/src/controller/build/properties/product_logging/vector-test.yaml new file mode 100644 index 00000000..c62a3807 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/product_logging/vector-test.yaml @@ -0,0 +1,136 @@ +# Run tests with `./test-vector.sh` +# +# A downside of these test cases is that they compare the whole event and that the message can +# contain source code positions in vector.yaml, e.g. "function call error for \"parse_xml\" at +# (584:643)". Please adapt the tests if you change VRL code in vector.yaml. +--- +tests: + - name: Test log4j XML log entry without throwable + inputs: + - type: log + insert_at: processed_files_log4j + log_fields: + file: /stackable/log/kafka/kafka.log4j.xml + message: > + started + (kafka.server.KafkaServer) + pod: kafka-broker-default-0 + source_type: file + timestamp: 2025-10-02T09:27:29.473487331Z + outputs: + - extract_from: extended_logs + conditions: + - type: vrl + source: | + expected_log_event = { + "cluster": "kafka", + "container": "kafka", + "file": "kafka.log4j.xml", + "level": "INFO", + "logger": "kafka.server.KafkaServer", + "message": "started (kafka.server.KafkaServer)", + "namespace": "default", + "pod": "kafka-broker-default-0", + "role": "broker", + "roleGroup": "default", + "timestamp": t'2025-10-02T09:27:28.582Z' + } + + assert_eq!(expected_log_event, .) + - name: Test log4j2 XML log entry without stacktrace + inputs: + - type: log + insert_at: processed_files_log4j2 + log_fields: + file: /stackable/log/kafka/kafka.log4j2.xml + message: > + started + (kafka.server.KafkaServer) + pod: kafka-broker-default-0 + source_type: file + timestamp: 2025-10-02T09:27:29.473487331Z + outputs: + - extract_from: extended_logs + conditions: + - type: vrl + source: | + expected_log_event = { + "cluster": "kafka", + "container": "kafka", + "file": "kafka.log4j2.xml", + "level": "INFO", + "logger": "kafka.server.KafkaServer", + "message": "started (kafka.server.KafkaServer)", + "namespace": "default", + "pod": "kafka-broker-default-0", + "role": "broker", + "roleGroup": "default", + "timestamp": t'2025-10-02T09:27:28.582Z' + } + + assert_eq!(expected_log_event, .) + - name: Test Vector internal logs + inputs: + - type: log + insert_at: filtered_logs_vector + log_fields: + arch: x86_64 + message: Vector has started. + metadata: + kind: event + level: INFO + module_path: vector::internal_events::process + target: vector + pid: 14 + pod: kafka-broker-default-0 + source_type: internal_logs + timestamp: 2025-10-02T09:46:14.479381097Z + version: 0.49.0 + outputs: + - extract_from: extended_logs + conditions: + - type: vrl + source: | + expected_log_event = { + "arch": "x86_64", + "cluster": "kafka", + "container": "vector", + "level": "INFO", + "logger": "vector::internal_events::process", + "message": "Vector has started.", + "namespace": "default", + "pod": "kafka-broker-default-0", + "role": "broker", + "roleGroup": "default", + "timestamp": "2025-10-02T09:46:14.479381097Z", + "version": "0.49.0" + } + + assert_eq!(expected_log_event, .) + - name: Test Vector internal log level filtering - INFO passes + inputs: + - type: log + insert_at: filtered_logs_vector + log_fields: + metadata: + level: INFO + outputs: + - extract_from: filtered_logs_vector + conditions: + - type: vrl + source: | + assert_eq!("INFO", .metadata.level) + - name: Test Vector internal log level filtering - DEBUG dropped + inputs: + - type: log + insert_at: filtered_logs_vector + log_fields: + metadata: + level: DEBUG + no_outputs_from: + - filtered_logs_vector diff --git a/rust/operator-binary/src/controller/build/properties/product_logging/vector.yaml b/rust/operator-binary/src/controller/build/properties/product_logging/vector.yaml new file mode 100644 index 00000000..4226a675 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/product_logging/vector.yaml @@ -0,0 +1,274 @@ +--- +data_dir: ${DATA_DIR} + +log_schema: + host_key: pod + +sources: + vector: + type: internal_logs + + files_log4j: + type: file + include: + - ${LOG_DIR}/*/*.log4j.xml + line_delimiter: "\r\n" + multiline: + mode: halt_before + start_pattern: ^" + raw_message + "" + parsed_event, err = parse_xml(wrapped_xml_event) + if err != null { + error = "XML not parsable: " + err + .errors = push(.errors, error) + log(error, level: "warn") + .message = raw_message + } else { + root = object!(parsed_event.root) + if !is_object(root.event) { + error = "Parsed event contains no \"event\" tag." + .errors = push(.errors, error) + log(error, level: "warn") + .message = raw_message + } else { + if keys(root) != ["event"] { + .errors = push(.errors, "Parsed event contains multiple tags: " + join!(keys(root), ", ")) + } + event = object!(root.event) + + epoch_milliseconds, err = to_int(event.@timestamp) + if err == null && epoch_milliseconds != 0 { + converted_timestamp, err = from_unix_timestamp(epoch_milliseconds, "milliseconds") + if err == null { + .timestamp = converted_timestamp + } else { + .errors = push(.errors, "Time not parsable, using current time instead: " + err) + } + } else { + .errors = push(.errors, "Timestamp not found, using current time instead.") + } + + .logger, err = string(event.@logger) + if err != null || is_empty(.logger) { + .errors = push(.errors, "Logger not found.") + } + + level, err = string(event.@level) + if err != null { + .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") + } else if !includes(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], level) { + .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") + } else { + .level = level + } + + message, err = string(event.message) + if err != null || is_empty(message) { + .errors = push(.errors, "Message not found.") + } + throwable = string(event.throwable) ?? "" + .message = join!(compact([message, throwable]), "\n") + } + } + + processed_files_log4j2: + inputs: + - files_log4j2 + type: remap + source: | + raw_message = string!(.message) + + .timestamp = now() + .logger = "" + .level = "INFO" + .message = "" + .errors = [] + + event = {} + parsed_event, err = parse_xml(raw_message) + if err != null { + error = "XML not parsable: " + err + .errors = push(.errors, error) + log(error, level: "warn") + .message = raw_message + } else { + if !is_object(parsed_event.Event) { + error = "Parsed event contains no \"Event\" tag." + .errors = push(.errors, error) + log(error, level: "warn") + .message = raw_message + } else { + event = object!(parsed_event.Event) + + tag_instant_valid = false + instant, err = object(event.Instant) + if err == null { + epoch_nanoseconds, err = to_int(instant.@epochSecond) * 1_000_000_000 + to_int(instant.@nanoOfSecond) + if err == null && epoch_nanoseconds != 0 { + converted_timestamp, err = from_unix_timestamp(epoch_nanoseconds, "nanoseconds") + if err == null { + .timestamp = converted_timestamp + tag_instant_valid = true + } else { + .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) + } + } else { + .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) + } + } + if !tag_instant_valid { + epoch_milliseconds, err = to_int(event.@timeMillis) + if err == null && epoch_milliseconds != 0 { + converted_timestamp, err = from_unix_timestamp(epoch_milliseconds, "milliseconds") + if err == null { + .timestamp = converted_timestamp + } else { + .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) + } + } else { + .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) + } + } + + .logger, err = string(event.@loggerName) + if err != null || is_empty(.logger) { + .errors = push(.errors, "Logger not found.") + } + + level, err = string(event.@level) + if err != null { + .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") + } else if !includes(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], level) { + .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") + } else { + .level = level + } + + exception = null + thrown = event.Thrown + if is_object(thrown) { + exception = "Exception" + thread, err = string(event.@thread) + if err == null && !is_empty(thread) { + exception = exception + " in thread \"" + thread + "\"" + } + thrown_name, err = string(thrown.@name) + if err == null && !is_empty(exception) { + exception = exception + " " + thrown_name + } + message = string(thrown.@localizedMessage) ?? + string(thrown.@message) ?? + "" + if !is_empty(message) { + exception = exception + ": " + message + } + stacktrace_items = array(thrown.ExtendedStackTrace.ExtendedStackTraceItem) ?? [] + stacktrace = "" + for_each(stacktrace_items) -> |_index, value| { + stacktrace = stacktrace + " " + class = string(value.@class) ?? "" + method = string(value.@method) ?? "" + if !is_empty(class) && !is_empty(method) { + stacktrace = stacktrace + "at " + class + "." + method + } + file = string(value.@file) ?? "" + line = string(value.@line) ?? "" + if !is_empty(file) && !is_empty(line) { + stacktrace = stacktrace + "(" + file + ":" + line + ")" + } + exact = to_bool(value.@exact) ?? false + location = string(value.@location) ?? "" + version = string(value.@version) ?? "" + if !is_empty(location) && !is_empty(version) { + stacktrace = stacktrace + " " + if !exact { + stacktrace = stacktrace + "~" + } + stacktrace = stacktrace + "[" + location + ":" + version + "]" + } + stacktrace = stacktrace + "\n" + } + if stacktrace != "" { + exception = exception + "\n" + stacktrace + } + } + + message, err = string(event.Message) + if err != null || is_empty(message) { + message = null + .errors = push(.errors, "Message not found.") + } + .message = join!(compact([message, exception]), "\n") + } + } + + extended_logs_files: + inputs: + - processed_files_* + type: remap + source: | + del(.source_type) + if .errors == [] { + del(.errors) + } + . |= parse_regex!(.file, r'^${LOG_DIR}/(?P.*?)/(?P.*?)$') + + filtered_logs_vector: + inputs: + - vector + type: filter + condition: '!includes(["TRACE", "DEBUG"], .metadata.level)' + + extended_logs_vector: + inputs: + - filtered_logs_vector + type: remap + source: | + .container = "vector" + .level = .metadata.level + .logger = .metadata.module_path + if exists(.file) { .processed_file = del(.file) } + del(.metadata) + del(.pid) + del(.source_type) + + extended_logs: + inputs: + - extended_logs_* + type: remap + source: | + .namespace = "${NAMESPACE}" + .cluster = "${CLUSTER_NAME}" + .role = "${ROLE_NAME}" + .roleGroup = "${ROLE_GROUP_NAME}" + +sinks: + aggregator: + inputs: + - extended_logs + type: vector + address: ${VECTOR_AGGREGATOR_ADDRESS} diff --git a/rust/operator-binary/src/controller/build/properties/security_properties.rs b/rust/operator-binary/src/controller/build/properties/security_properties.rs new file mode 100644 index 00000000..be9ab791 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/security_properties.rs @@ -0,0 +1,76 @@ +//! Builder for `security.properties` (the JVM security properties file). +//! +//! `networkaddress.cache.ttl` and `networkaddress.cache.negative.ttl` are `required` +//! properties with recommended values, so they are always emitted. + +use std::collections::BTreeMap; + +const NETWORKADDRESS_CACHE_TTL: &str = "networkaddress.cache.ttl"; +const NETWORKADDRESS_CACHE_NEGATIVE_TTL: &str = "networkaddress.cache.negative.ttl"; + +const DEFAULT_NETWORKADDRESS_CACHE_TTL: &str = "30"; +const DEFAULT_NETWORKADDRESS_CACHE_NEGATIVE_TTL: &str = "0"; + +/// Build the `security.properties` key/value pairs. +/// +/// `overrides` are the resolved user overrides for `security.properties` +/// (highest precedence). +pub fn build(overrides: BTreeMap) -> BTreeMap { + let mut props: BTreeMap = BTreeMap::new(); + + // 1. Defaults (recommended values for the required properties). + props.insert( + NETWORKADDRESS_CACHE_TTL.to_string(), + DEFAULT_NETWORKADDRESS_CACHE_TTL.to_string(), + ); + props.insert( + NETWORKADDRESS_CACHE_NEGATIVE_TTL.to_string(), + DEFAULT_NETWORKADDRESS_CACHE_NEGATIVE_TTL.to_string(), + ); + + // 2. User overrides (highest precedence). + for (k, v) in overrides { + props.insert(k, v); + } + + props +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn defaults_present_without_overrides() { + let props = build(BTreeMap::new()); + assert_eq!( + props.get("networkaddress.cache.ttl"), + Some(&"30".to_string()) + ); + assert_eq!( + props.get("networkaddress.cache.negative.ttl"), + Some(&"0".to_string()) + ); + } + + #[test] + fn user_override_wins() { + let overrides = [("networkaddress.cache.ttl".to_string(), "60".to_string())] + .into_iter() + .collect(); + let props = build(overrides); + assert_eq!( + props.get("networkaddress.cache.ttl"), + Some(&"60".to_string()) + ); + } + + #[test] + fn extra_user_override_key_is_added() { + let overrides = [("custom.security.prop".to_string(), "x".to_string())] + .into_iter() + .collect(); + let props = build(overrides); + assert_eq!(props.get("custom.security.prop"), Some(&"x".to_string())); + } +} diff --git a/rust/operator-binary/src/controller/build/resource/config_map.rs b/rust/operator-binary/src/controller/build/resource/config_map.rs new file mode 100644 index 00000000..392e0961 --- /dev/null +++ b/rust/operator-binary/src/controller/build/resource/config_map.rs @@ -0,0 +1,251 @@ +use indoc::formatdoc; +use snafu::{ResultExt, Snafu}; +use stackable_operator::{ + builder::{configmap::ConfigMapBuilder, meta::ObjectMetaBuilder}, + k8s_openapi::api::core::v1::ConfigMap, + product_logging::framework::VECTOR_CONFIG_FILE, + v2::{ + builder::meta::ownerreference_from_resource, + config_file_writer::{PropertiesWriterError, to_java_properties_string}, + }, +}; + +use crate::{ + controller::{ + RoleGroupName, ValidatedCluster, ValidatedRoleGroupConfig, + build::{ + properties::{ + ConfigFileName, config_file_name, product_logging::role_group_config_map_data, + }, + security::client_properties, + }, + }, + crd::{ + STACKABLE_LISTENER_BOOTSTRAP_DIR, STACKABLE_LISTENER_BROKER_DIR, + listener::{KafkaListenerConfig, node_address_cmd}, + role::AnyConfig, + }, +}; + +#[derive(Snafu, Debug)] +pub enum Error { + #[snafu(display("failed to build ConfigMap for role group {role_group}"))] + BuildRoleGroupConfig { + source: stackable_operator::builder::configmap::Error, + role_group: RoleGroupName, + }, + + #[snafu(display( + "failed to serialize [{}] for role group {role_group}", + ConfigFileName::Security + ))] + JvmSecurityProperties { + source: PropertiesWriterError, + role_group: RoleGroupName, + }, + + #[snafu(display("failed to serialize config for role group {role_group}"))] + SerializeConfig { + source: PropertiesWriterError, + role_group: RoleGroupName, + }, + + #[snafu(display("failed to build pod descriptors"))] + BuildPodDescriptors { + source: crate::controller::PodDescriptorsError, + }, + + #[snafu(display("no Kraft controllers found to build"))] + NoKraftControllersFound, +} + +/// The rolegroup [`ConfigMap`] configures the rolegroup based on the configuration given by the administrator. +/// +/// `vector_config` is the static Vector agent config (`vector.yaml`) added by the caller; it is +/// `None` when the Vector agent is disabled. Resource naming and labels use the role (derived +/// from `validated_rg.config`) and the typed `role_group_name`. +pub fn build_rolegroup_config_map( + validated_cluster: &ValidatedCluster, + role_group_name: &RoleGroupName, + validated_rg: &ValidatedRoleGroupConfig, + listener_config: &KafkaListenerConfig, + vector_config: Option, +) -> Result { + let role = validated_rg.config.config.kafka_role(); + let cluster_config = &validated_cluster.cluster_config; + let kafka_security = &cluster_config.kafka_security; + let resolved_product_image = &validated_cluster.image; + let kafka_config_file_name = config_file_name(&validated_rg.config.config).to_string(); + let config_overrides = validated_rg + .config_overrides + .config_file_overrides() + .overrides + .clone(); + + let pod_descriptors = validated_cluster + .pod_descriptors(None) + .context(BuildPodDescriptorsSnafu)?; + + if cluster_config.is_kraft_mode() && pod_descriptors.is_empty() { + return NoKraftControllersFoundSnafu.fail(); + } + + let kafka_config = match &validated_rg.config.config { + AnyConfig::Broker(_) => crate::controller::build::properties::broker_properties::build( + cluster_config, + listener_config, + &pod_descriptors, + config_overrides, + ), + AnyConfig::Controller(_) => { + crate::controller::build::properties::controller_properties::build( + cluster_config, + listener_config, + &pod_descriptors, + config_overrides, + ) + } + }; + + // The `networkaddress.cache.*` defaults are always emitted (with user `security.properties` + // overrides winning); see `security_properties::build`. + let jvm_sec_props = crate::controller::build::properties::security_properties::build( + validated_rg + .config_overrides + .security_properties() + .overrides + .clone(), + ); + + let mut cm_builder = ConfigMapBuilder::new(); + cm_builder + .metadata( + ObjectMetaBuilder::new() + .name_and_namespace(validated_cluster) + .name( + validated_cluster + .resource_names(&role, role_group_name) + .role_group_config_map() + .to_string(), + ) + .ownerreference(ownerreference_from_resource( + validated_cluster, + None, + Some(true), + )) + .with_labels(validated_cluster.recommended_labels(&role, role_group_name)) + .build(), + ) + .add_data( + kafka_config_file_name, + to_java_properties_string(kafka_config.iter()).with_context(|_| { + SerializeConfigSnafu { + role_group: role_group_name.clone(), + } + })?, + ) + .add_data( + ConfigFileName::Security.to_string(), + to_java_properties_string(jvm_sec_props.iter()).with_context(|_| { + JvmSecurityPropertiesSnafu { + role_group: role_group_name.clone(), + } + })?, + ) + .add_data( + ConfigFileName::Client.to_string(), + to_java_properties_string( + client_properties(kafka_security) + .iter() + .filter_map(|(k, v)| v.as_ref().map(|v| (k, v))), + ) + .with_context(|_| JvmSecurityPropertiesSnafu { + role_group: role_group_name.clone(), + })?, + ) + // This file contains the JAAS configuration for Kerberos authentication + // It has the ".properties" extension but is not a Java properties file. + // It is processed by `config-utils` to substitute "env:" and "file:" variables + // and this tool currently doesn't support the JAAS login configuration format. + .add_data( + ConfigFileName::Jaas.to_string(), + jaas_config_file(kafka_security.has_kerberos_enabled()), + ); + + tracing::debug!(?kafka_config, "Applied kafka config"); + tracing::debug!(?jvm_sec_props, "Applied JVM config"); + + let config_data = role_group_config_map_data( + &resolved_product_image.product_version, + &validated_rg.config.config, + ); + for (file_name, data) in config_data { + if let Some(data) = data { + cm_builder.add_data(file_name, data); + } + } + + if let Some(vector_config) = vector_config { + cm_builder.add_data(VECTOR_CONFIG_FILE, vector_config); + } + + cm_builder + .build() + .with_context(|_| BuildRoleGroupConfigSnafu { + role_group: role_group_name.clone(), + }) +} + +// Generate JAAS configuration file for Kerberos authentication +// or an empty string if Kerberos is not enabled. +// See https://docs.oracle.com/javase/8/docs/technotes/guides/security/jgss/tutorials/LoginConfigFile.html +fn jaas_config_file(is_kerberos_enabled: bool) -> String { + match is_kerberos_enabled { + false => String::new(), + true => formatdoc! {" + bootstrap.KafkaServer {{ + com.sun.security.auth.module.Krb5LoginModule required + useKeyTab=true + storeKey=true + isInitiator=false + keyTab=\"/stackable/kerberos/keytab\" + principal=\"kafka/{bootstrap_address}@${{env:KERBEROS_REALM}}\"; + }}; + + client.KafkaServer {{ + com.sun.security.auth.module.Krb5LoginModule required + useKeyTab=true + storeKey=true + isInitiator=false + keyTab=\"/stackable/kerberos/keytab\" + principal=\"kafka/{broker_address}@${{env:KERBEROS_REALM}}\"; + }}; + + ", + bootstrap_address = node_address_cmd(STACKABLE_LISTENER_BOOTSTRAP_DIR), + broker_address = node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), + }, + } +} + +#[cfg(test)] +mod tests { + use super::jaas_config_file; + + #[test] + fn jaas_config_file_empty_without_kerberos() { + assert_eq!(jaas_config_file(false), ""); + } + + #[test] + fn jaas_config_file_renders_bootstrap_and_client_sections_with_kerberos() { + let jaas = jaas_config_file(true); + assert!(jaas.contains("bootstrap.KafkaServer")); + assert!(jaas.contains("client.KafkaServer")); + assert!(jaas.contains("Krb5LoginModule")); + assert!(jaas.contains("${env:KERBEROS_REALM}")); + // The bootstrap and client principals embed distinct listener addresses. + assert!(jaas.contains("/stackable/listener-bootstrap")); + assert!(jaas.contains("/stackable/listener-broker")); + } +} diff --git a/rust/operator-binary/src/discovery.rs b/rust/operator-binary/src/controller/build/resource/discovery.rs similarity index 60% rename from rust/operator-binary/src/discovery.rs rename to rust/operator-binary/src/controller/build/resource/discovery.rs index 3730de33..0c781694 100644 --- a/rust/operator-binary/src/discovery.rs +++ b/rust/operator-binary/src/controller/build/resource/discovery.rs @@ -1,31 +1,20 @@ -use std::num::TryFromIntError; +use std::{num::TryFromIntError, str::FromStr}; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ builder::{configmap::ConfigMapBuilder, meta::ObjectMetaBuilder}, - commons::product_image_selection::ResolvedProductImage, crd::listener, k8s_openapi::api::core::v1::ConfigMap, - kube::{Resource, ResourceExt, runtime::reflector::ObjectRef}, + v2::builder::meta::ownerreference_from_resource, }; use crate::{ - controller::KAFKA_CONTROLLER_NAME, - crd::{role::KafkaRole, security::KafkaTlsSecurity, v1alpha1}, - utils::build_recommended_labels, + controller::{RoleGroupName, ValidatedCluster}, + crd::role::KafkaRole, }; #[derive(Snafu, Debug)] pub enum Error { - #[snafu(display("object {} is missing metadata to build owner reference", kafka))] - ObjectMissingMetadataForOwnerRef { - source: stackable_operator::builder::meta::Error, - kafka: ObjectRef, - }, - - #[snafu(display("object has no name associated"))] - NoName, - #[snafu(display("could not find service port with name {}", port_name))] NoServicePort { port_name: String }, @@ -36,22 +25,16 @@ pub enum Error { BuildConfigMap { source: stackable_operator::builder::configmap::Error, }, - - #[snafu(display("failed to build metadata"))] - MetadataBuild { - source: stackable_operator::builder::meta::Error, - }, } /// Build a discovery [`ConfigMap`] containing information about how to connect to a certain /// [`v1alpha1::KafkaCluster`]. pub fn build_discovery_configmap( - kafka: &v1alpha1::KafkaCluster, - owner: &impl Resource, - resolved_product_image: &ResolvedProductImage, - kafka_security: &KafkaTlsSecurity, + validated_cluster: &ValidatedCluster, listeners: &[listener::v1alpha1::Listener], ) -> Result { + let kafka_security = &validated_cluster.cluster_config.kafka_security; + let port_name = if kafka_security.has_kerberos_enabled() { kafka_security.bootstrap_port_name() } else { @@ -68,20 +51,19 @@ pub fn build_discovery_configmap( ConfigMapBuilder::new() .metadata( ObjectMetaBuilder::new() - .name_and_namespace(kafka) - .name(owner.name_unchecked()) - .ownerreference_from_resource(owner, None, Some(true)) - .with_context(|_| ObjectMissingMetadataForOwnerRefSnafu { - kafka: ObjectRef::from_obj(kafka), - })? - .with_recommended_labels(&build_recommended_labels( - kafka, - KAFKA_CONTROLLER_NAME, - &resolved_product_image.product_version, - &KafkaRole::Broker.to_string(), - "discovery", + .name_and_namespace(validated_cluster) + .ownerreference(ownerreference_from_resource( + validated_cluster, + None, + Some(true), )) - .context(MetadataBuildSnafu)? + .with_labels( + validated_cluster.recommended_labels( + &KafkaRole::Broker, + &RoleGroupName::from_str("discovery") + .expect("'discovery' is a valid role group name"), + ), + ) .build(), ) .add_data("KAFKA", bootstrap_servers) diff --git a/rust/operator-binary/src/controller/build/resource/listener.rs b/rust/operator-binary/src/controller/build/resource/listener.rs new file mode 100644 index 00000000..6e5adc88 --- /dev/null +++ b/rust/operator-binary/src/controller/build/resource/listener.rs @@ -0,0 +1,58 @@ +use stackable_operator::{ + builder::meta::ObjectMetaBuilder, crd::listener, + v2::builder::meta::ownerreference_from_resource, +}; + +use crate::{ + controller::{RoleGroupName, ValidatedCluster, security::ValidatedKafkaSecurity}, + crd::role::{KafkaRole, broker::BrokerConfig}, +}; + +/// Kafka clients will use the load-balanced bootstrap listener to get a list of broker addresses and will use those to +/// transmit data to the correct broker. +// TODO (@NickLarsenNZ): Move shared functionality to stackable-operator +pub fn build_broker_rolegroup_bootstrap_listener( + validated_cluster: &ValidatedCluster, + role: &KafkaRole, + role_group_name: &RoleGroupName, + merged_config: &BrokerConfig, +) -> listener::v1alpha1::Listener { + let kafka_security = &validated_cluster.cluster_config.kafka_security; + + listener::v1alpha1::Listener { + metadata: ObjectMetaBuilder::new() + .name_and_namespace(validated_cluster) + .name(validated_cluster.bootstrap_listener_name(role, role_group_name)) + .ownerreference(ownerreference_from_resource( + validated_cluster, + None, + Some(true), + )) + .with_labels(validated_cluster.recommended_labels(role, role_group_name)) + .build(), + spec: listener::v1alpha1::ListenerSpec { + class_name: Some(merged_config.bootstrap_listener_class.to_string()), + ports: Some(bootstrap_listener_ports(kafka_security)), + ..listener::v1alpha1::ListenerSpec::default() + }, + status: None, + } +} + +fn bootstrap_listener_ports( + kafka_security: &ValidatedKafkaSecurity, +) -> Vec { + vec![if kafka_security.has_kerberos_enabled() { + listener::v1alpha1::ListenerPort { + name: kafka_security.bootstrap_port_name().to_string(), + port: kafka_security.bootstrap_port().into(), + protocol: Some("TCP".to_string()), + } + } else { + listener::v1alpha1::ListenerPort { + name: kafka_security.client_port_name().to_string(), + port: kafka_security.client_port().into(), + protocol: Some("TCP".to_string()), + } + }] +} diff --git a/rust/operator-binary/src/controller/build/resource/mod.rs b/rust/operator-binary/src/controller/build/resource/mod.rs new file mode 100644 index 00000000..2d0977ad --- /dev/null +++ b/rust/operator-binary/src/controller/build/resource/mod.rs @@ -0,0 +1,9 @@ +//! Builders that assemble Kubernetes resources for kafka rolegroups. + +pub mod config_map; +pub mod discovery; +pub mod listener; +pub mod pdb; +pub mod rbac; +pub mod service; +pub mod statefulset; diff --git a/rust/operator-binary/src/controller/build/resource/pdb.rs b/rust/operator-binary/src/controller/build/resource/pdb.rs new file mode 100644 index 00000000..29bd473e --- /dev/null +++ b/rust/operator-binary/src/controller/build/resource/pdb.rs @@ -0,0 +1,45 @@ +use stackable_operator::{ + commons::pdb::PdbConfig, k8s_openapi::api::policy::v1::PodDisruptionBudget, + v2::builder::pdb::pod_disruption_budget_builder_with_role, +}; + +use crate::{ + controller::{ValidatedCluster, controller_name, operator_name, product_name}, + crd::role::KafkaRole, +}; + +/// Builds the [`PodDisruptionBudget`] for the given `role`, or `None` if PDBs are disabled. +pub fn build_pdb( + pdb: &PdbConfig, + validated_cluster: &ValidatedCluster, + role: &KafkaRole, +) -> Option { + if !pdb.enabled { + return None; + } + let max_unavailable = pdb.max_unavailable.unwrap_or(match role { + KafkaRole::Broker => max_unavailable_brokers(), + KafkaRole::Controller => max_unavailable_controllers(), + }); + let pdb = pod_disruption_budget_builder_with_role( + validated_cluster, + &product_name(), + &ValidatedCluster::role_name(role), + &operator_name(), + &controller_name(), + ) + .with_max_unavailable(max_unavailable) + .build(); + + Some(pdb) +} + +fn max_unavailable_brokers() -> u16 { + // We can not make any assumptions about topic replication factors. + 1 +} + +fn max_unavailable_controllers() -> u16 { + // TODO: what do we want here? + 1 +} diff --git a/rust/operator-binary/src/controller/build/resource/rbac.rs b/rust/operator-binary/src/controller/build/resource/rbac.rs new file mode 100644 index 00000000..175148f3 --- /dev/null +++ b/rust/operator-binary/src/controller/build/resource/rbac.rs @@ -0,0 +1,81 @@ +//! Builds the cluster-wide RBAC resources (`ServiceAccount` and `RoleBinding`). +//! +//! The names come from [`ResourceNames`](stackable_operator::v2::role_utils::ResourceNames) +//! and are identical to the previously used `commons::rbac::build_rbac_resources` +//! (`-serviceaccount`, `-rolebinding`, `-clusterrole`), so switching to +//! this builder does not rename any RBAC objects. + +use stackable_operator::{ + builder::meta::ObjectMetaBuilder, + k8s_openapi::api::{ + core::v1::ServiceAccount, + rbac::v1::{RoleBinding, RoleRef, Subject}, + }, + kvp::Labels, + v2::{builder::meta::ownerreference_from_resource, role_utils::ResourceNames}, +}; + +use crate::controller::{ValidatedCluster, product_name}; + +/// Type-safe RBAC resource names for this cluster. +fn rbac_resource_names(validated_cluster: &ValidatedCluster) -> ResourceNames { + ResourceNames { + cluster_name: validated_cluster.name.clone(), + product_name: product_name(), + } +} + +/// Builds the [`ServiceAccount`] shared by all role groups, named `-serviceaccount`. +pub fn build_rbac_service_account( + validated_cluster: &ValidatedCluster, + labels: Labels, +) -> ServiceAccount { + let resource_names = rbac_resource_names(validated_cluster); + + ServiceAccount { + metadata: ObjectMetaBuilder::new() + .name_and_namespace(validated_cluster) + .name(resource_names.service_account_name().to_string()) + .ownerreference(ownerreference_from_resource( + validated_cluster, + None, + Some(true), + )) + .with_labels(labels) + .build(), + ..ServiceAccount::default() + } +} + +/// Builds the [`RoleBinding`] (named `-rolebinding`) that binds the +/// [`ServiceAccount`] to the `-clusterrole` `ClusterRole`. +pub fn build_rbac_role_binding( + validated_cluster: &ValidatedCluster, + labels: Labels, +) -> RoleBinding { + let resource_names = rbac_resource_names(validated_cluster); + + RoleBinding { + metadata: ObjectMetaBuilder::new() + .name_and_namespace(validated_cluster) + .name(resource_names.role_binding_name().to_string()) + .ownerreference(ownerreference_from_resource( + validated_cluster, + None, + Some(true), + )) + .with_labels(labels) + .build(), + role_ref: RoleRef { + api_group: "rbac.authorization.k8s.io".to_string(), + kind: "ClusterRole".to_string(), + name: resource_names.cluster_role_name().to_string(), + }, + subjects: Some(vec![Subject { + kind: "ServiceAccount".to_string(), + name: resource_names.service_account_name().to_string(), + namespace: Some(validated_cluster.namespace.to_string()), + ..Subject::default() + }]), + } +} diff --git a/rust/operator-binary/src/controller/build/resource/service.rs b/rust/operator-binary/src/controller/build/resource/service.rs new file mode 100644 index 00000000..60f10b16 --- /dev/null +++ b/rust/operator-binary/src/controller/build/resource/service.rs @@ -0,0 +1,130 @@ +use stackable_operator::{ + builder::meta::ObjectMetaBuilder, + k8s_openapi::api::core::v1::{Service, ServicePort, ServiceSpec}, + kvp::{Annotations, Labels}, + v2::builder::meta::ownerreference_from_resource, +}; + +use crate::{ + controller::{RoleGroupName, ValidatedCluster, security::ValidatedKafkaSecurity}, + crd::{METRICS_PORT, METRICS_PORT_NAME, role::KafkaRole}, +}; + +/// The rolegroup [`Service`] is a headless service that allows direct access to the instances of a certain rolegroup +/// +/// This is mostly useful for internal communication between peers, or for clients that perform client-side load balancing. +pub fn build_rolegroup_headless_service( + validated_cluster: &ValidatedCluster, + role: &KafkaRole, + role_group_name: &RoleGroupName, + kafka_security: &ValidatedKafkaSecurity, +) -> Service { + Service { + metadata: ObjectMetaBuilder::new() + .name_and_namespace(validated_cluster) + .name( + validated_cluster + .resource_names(role, role_group_name) + .headless_service_name() + .to_string(), + ) + .ownerreference(ownerreference_from_resource( + validated_cluster, + None, + Some(true), + )) + .with_labels(validated_cluster.recommended_labels(role, role_group_name)) + .build(), + spec: Some(ServiceSpec { + cluster_ip: Some("None".to_string()), + ports: Some(headless_ports(kafka_security)), + selector: Some( + validated_cluster + .role_group_selector(role, role_group_name) + .into(), + ), + publish_not_ready_addresses: Some(true), + ..ServiceSpec::default() + }), + status: None, + } +} + +/// The rolegroup metrics [`Service`] is a service that exposes metrics and a prometheus scraping label +pub fn build_rolegroup_metrics_service( + validated_cluster: &ValidatedCluster, + role: &KafkaRole, + role_group_name: &RoleGroupName, +) -> Service { + Service { + metadata: ObjectMetaBuilder::new() + .name_and_namespace(validated_cluster) + .name( + validated_cluster + .resource_names(role, role_group_name) + .metrics_service_name() + .to_string(), + ) + .ownerreference(ownerreference_from_resource( + validated_cluster, + None, + Some(true), + )) + .with_labels(validated_cluster.recommended_labels(role, role_group_name)) + .with_labels(prometheus_labels()) + .with_annotations(prometheus_annotations()) + .build(), + spec: Some(ServiceSpec { + // Internal communication does not need to be exposed + type_: Some("ClusterIP".to_string()), + cluster_ip: Some("None".to_string()), + ports: Some(metrics_ports()), + selector: Some( + validated_cluster + .role_group_selector(role, role_group_name) + .into(), + ), + publish_not_ready_addresses: Some(true), + ..ServiceSpec::default() + }), + status: None, + } +} + +fn metrics_ports() -> Vec { + vec![ServicePort { + name: Some(METRICS_PORT_NAME.to_string()), + port: METRICS_PORT.into(), + protocol: Some("TCP".to_string()), + ..ServicePort::default() + }] +} + +fn headless_ports(kafka_security: &ValidatedKafkaSecurity) -> Vec { + vec![ServicePort { + name: Some(kafka_security.client_port_name().into()), + port: kafka_security.client_port().into(), + protocol: Some("TCP".to_string()), + ..ServicePort::default() + }] +} + +/// Common labels for Prometheus +fn prometheus_labels() -> Labels { + Labels::try_from([("prometheus.io/scrape", "true")]).expect("should be a valid label") +} + +/// Common annotations for Prometheus +/// +/// These annotations can be used in a ServiceMonitor. +/// +/// see also +fn prometheus_annotations() -> Annotations { + Annotations::try_from([ + ("prometheus.io/path".to_owned(), "/metrics".to_owned()), + ("prometheus.io/port".to_owned(), METRICS_PORT.to_string()), + ("prometheus.io/scheme".to_owned(), "http".to_owned()), + ("prometheus.io/scrape".to_owned(), "true".to_owned()), + ]) + .expect("should be valid annotations") +} diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs new file mode 100644 index 00000000..a2827aa4 --- /dev/null +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -0,0 +1,814 @@ +use std::{ops::Deref, str::FromStr}; + +use snafu::{OptionExt, ResultExt, Snafu}; +use stackable_operator::{ + builder::{ + meta::ObjectMetaBuilder, + pod::{ + PodBuilder, container::ContainerBuilder, resources::ResourceRequirementsBuilder, + security::PodSecurityContextBuilder, volume::VolumeBuilder, + }, + }, + commons::product_image_selection::ResolvedProductImage, + constants::RESTART_CONTROLLER_ENABLED_LABEL, + k8s_openapi::{ + DeepMerge, + api::{ + apps::v1::{StatefulSet, StatefulSetSpec, StatefulSetUpdateStrategy}, + core::v1::{ + ConfigMapKeySelector, ConfigMapVolumeSource, ContainerPort, EnvVar, EnvVarSource, + ExecAction, ObjectFieldSelector, PodSpec, Probe, ServiceAccount, TCPSocketAction, + Volume, + }, + }, + apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString}, + }, + kube::ResourceExt, + product_logging, + v2::{ + builder::{ + meta::ownerreference_from_resource, + pod::{ + container::EnvVarSet, + volume::{ListenerReference, listener_operator_volume_source_builder_build_pvc}, + }, + }, + jvm_argument_overrides::JvmArgumentOverrides, + product_logging::framework::{ + STACKABLE_LOG_DIR, ValidatedContainerLogConfigChoice, vector_container, + }, + role_group_utils::ResourceNames, + types::kubernetes::{ContainerName, PersistentVolumeClaimName, VolumeName}, + }, +}; + +use crate::{ + controller::{ + RoleGroupName, ValidatedCluster, ValidatedRoleGroupConfig, + build::{ + command::{ + broker_kafka_container_commands, controller_kafka_container_command, + kafka_log_opts, kafka_log_opts_env_var, + }, + graceful_shutdown::add_graceful_shutdown_config, + kerberos::add_kerberos_pod_config, + properties::product_logging::MAX_KAFKA_LOG_FILES_SIZE, + security::{ + add_broker_volume_and_volume_mounts, add_controller_volume_and_volume_mounts, + kcat_prober_container_commands, + }, + }, + node_id_hasher::node_id_hash32_offset, + security::ValidatedKafkaSecurity, + validate::ValidatedLogging, + }, + crd::{ + BROKER_ID_POD_MAP_DIR, BROKER_ID_POD_MAP_DIR_NAME, KAFKA_HEAP_OPTS, + LISTENER_BOOTSTRAP_VOLUME_NAME, LISTENER_BROKER_VOLUME_NAME, LOG_DIRS_VOLUME_NAME, + METRICS_PORT, METRICS_PORT_NAME, STACKABLE_CONFIG_DIR, STACKABLE_CONFIG_DIR_NAME, + STACKABLE_DATA_DIR, STACKABLE_LISTENER_BOOTSTRAP_DIR, STACKABLE_LISTENER_BROKER_DIR, + STACKABLE_LOG_CONFIG_DIR, STACKABLE_LOG_CONFIG_DIR_NAME, STACKABLE_LOG_DIR_NAME, + role::{ + AnyConfig, KAFKA_NODE_ID_OFFSET, KafkaRole, broker::BrokerContainer, + controller::ControllerContainer, + }, + }, +}; + +stackable_operator::constant!(VECTOR_CONTAINER_NAME: ContainerName = "vector"); +// The Vector container reads its `vector.yaml` from the `config` volume (the rolegroup +// ConfigMap) and tails product logs from the `log` volume. +stackable_operator::constant!(VECTOR_CONFIG_VOLUME_NAME: VolumeName = "config"); +stackable_operator::constant!(VECTOR_LOG_VOLUME_NAME: VolumeName = "log"); + +/// Name of both the env var and the ZooKeeper discovery ConfigMap key holding the +/// ZooKeeper connection string. +const ZOOKEEPER_ENV_VAR_NAME: &str = "ZOOKEEPER"; +const POD_MANAGEMENT_POLICY_PARALLEL: &str = "Parallel"; + +#[derive(Snafu, Debug)] +pub enum Error { + #[snafu(display("failed to add kerberos config"))] + AddKerberosConfig { + source: crate::controller::build::kerberos::Error, + }, + + #[snafu(display("failed to add listener volume"))] + AddListenerVolume { + source: stackable_operator::builder::pod::Error, + }, + + #[snafu(display("failed to add Secret Volumes and VolumeMounts"))] + AddVolumesAndVolumeMounts { + source: crate::controller::build::security::Error, + }, + + #[snafu(display("failed to add needed volumeMount"))] + AddVolumeMount { + source: stackable_operator::builder::pod::container::Error, + }, + + #[snafu(display("failed to add needed volume"))] + AddVolume { + source: stackable_operator::builder::pod::Error, + }, + + #[snafu(display("failed to build pod descriptors"))] + BuildPodDescriptors { + source: crate::controller::PodDescriptorsError, + }, + + #[snafu(display("failed to construct JVM arguments"))] + ConstructJvmArguments { + source: crate::controller::build::jvm::Error, + }, + + #[snafu(display("failed to configure graceful shutdown"))] + GracefulShutdown { + source: crate::controller::build::graceful_shutdown::Error, + }, + + #[snafu(display("invalid Container name [{name}]"))] + InvalidContainerName { + name: String, + source: stackable_operator::builder::pod::container::Error, + }, + + #[snafu(display("missing secret lifetime"))] + MissingSecretLifetime, +} + +/// The broker rolegroup [`StatefulSet`] runs the rolegroup, as configured by the administrator. +/// +/// The [`Pod`](`stackable_operator::k8s_openapi::api::core::v1::Pod`)s are accessible through the corresponding +/// [`Service`](`stackable_operator::k8s_openapi::api::core::v1::Service`) from [`build_rolegroup_headless_service`](`crate::controller::build::resource::service::build_rolegroup_headless_service`). +pub fn build_broker_rolegroup_statefulset( + kafka_role: &KafkaRole, + role_group_name: &RoleGroupName, + validated_cluster: &ValidatedCluster, + validated_rg: &ValidatedRoleGroupConfig, + service_account: &ServiceAccount, +) -> Result { + let kafka_security = &validated_cluster.cluster_config.kafka_security; + let resolved_product_image = &validated_cluster.image; + let merged_config = &validated_rg.config.config; + let resource_names = validated_cluster.resource_names(kafka_role, role_group_name); + let recommended_labels = validated_cluster.recommended_labels(kafka_role, role_group_name); + // Used for PVC templates that cannot be modified once they are deployed + let unversioned_recommended_labels = + validated_cluster.unversioned_recommended_labels(kafka_role, role_group_name); + + let kcat_prober_container_name = BrokerContainer::KcatProber.to_string(); + let mut cb_kcat_prober = + ContainerBuilder::new(&kcat_prober_container_name).context(InvalidContainerNameSnafu { + name: kcat_prober_container_name.clone(), + })?; + + let kafka_container_name = BrokerContainer::Kafka.to_string(); + let mut cb_kafka = + ContainerBuilder::new(&kafka_container_name).context(InvalidContainerNameSnafu { + name: kafka_container_name.clone(), + })?; + + let mut pod_builder = PodBuilder::new(); + + // Add TLS related volumes and volume mounts + let requested_secret_lifetime = merged_config + .deref() + .requested_secret_lifetime + .context(MissingSecretLifetimeSnafu)?; + add_broker_volume_and_volume_mounts( + kafka_security, + &mut pod_builder, + &mut cb_kcat_prober, + &mut cb_kafka, + &requested_secret_lifetime, + ) + .context(AddVolumesAndVolumeMountsSnafu)?; + + let mut pvcs = merged_config.resources().storage.build_pvcs(); + + // bootstrap listener should be persistent, + // main broker listener is an ephemeral PVC instead + let bootstrap_listener_name = + validated_cluster.bootstrap_listener_name(kafka_role, role_group_name); + let bootstrap_pvc_name = PersistentVolumeClaimName::from_str(LISTENER_BOOTSTRAP_VOLUME_NAME) + .expect("the bootstrap listener volume name is a valid PVC name"); + pvcs.push(listener_operator_volume_source_builder_build_pvc( + &ListenerReference::Listener(bootstrap_listener_name), + &unversioned_recommended_labels, + &bootstrap_pvc_name, + )); + + if kafka_security.has_kerberos_enabled() { + add_kerberos_pod_config( + kafka_security, + kafka_role, + &mut cb_kcat_prober, + &mut cb_kafka, + &mut pod_builder, + ) + .context(AddKerberosConfigSnafu)?; + } + + let mut env = Vec::::from(validated_rg.env_overrides.clone()); + + if let Some(zookeeper_config_map_name) = + &validated_cluster.cluster_config.zookeeper_config_map_name + { + env.push(EnvVar { + name: ZOOKEEPER_ENV_VAR_NAME.to_string(), + value_from: Some(EnvVarSource { + config_map_key_ref: Some(ConfigMapKeySelector { + name: zookeeper_config_map_name.to_string(), + key: ZOOKEEPER_ENV_VAR_NAME.to_string(), + ..ConfigMapKeySelector::default() + }), + ..EnvVarSource::default() + }), + ..EnvVar::default() + }) + }; + + env.push(EnvVar { + name: "POD_NAME".to_string(), + value_from: Some(EnvVarSource { + field_ref: Some(ObjectFieldSelector { + api_version: Some("v1".to_string()), + field_path: "metadata.name".to_string(), + }), + ..EnvVarSource::default() + }), + ..EnvVar::default() + }); + + cb_kafka + .image_from_product_image(resolved_product_image) + .command(vec![ + "/bin/bash".to_string(), + "-x".to_string(), + "-euo".to_string(), + "pipefail".to_string(), + "-c".to_string(), + ]) + .args(vec![broker_kafka_container_commands( + validated_cluster.cluster_config.is_kraft_mode(), + // we need controller pods + validated_cluster + .pod_descriptors(Some(&KafkaRole::Controller)) + .context(BuildPodDescriptorsSnafu)?, + kafka_security, + &resolved_product_image.product_version, + )]); + + add_common_kafka_env( + &mut cb_kafka, + merged_config, + &validated_rg + .product_specific_common_config + .jvm_argument_overrides, + resolved_product_image, + kafka_role, + role_group_name, + )?; + + cb_kafka + .add_env_var( + "KAFKA_CLIENT_PORT".to_string(), + kafka_security.client_port().to_string(), + ) + .add_env_vars(env) + .add_container_ports(container_ports(kafka_security)) + .add_volume_mount(LOG_DIRS_VOLUME_NAME, STACKABLE_DATA_DIR) + .context(AddVolumeMountSnafu)? + .add_volume_mount(STACKABLE_CONFIG_DIR_NAME, STACKABLE_CONFIG_DIR) + .context(AddVolumeMountSnafu)? + .add_volume_mount( + LISTENER_BOOTSTRAP_VOLUME_NAME, + STACKABLE_LISTENER_BOOTSTRAP_DIR, + ) + .context(AddVolumeMountSnafu)? + .add_volume_mount(LISTENER_BROKER_VOLUME_NAME, STACKABLE_LISTENER_BROKER_DIR) + .context(AddVolumeMountSnafu)? + .add_volume_mount(STACKABLE_LOG_CONFIG_DIR_NAME, STACKABLE_LOG_CONFIG_DIR) + .context(AddVolumeMountSnafu)? + .add_volume_mount(STACKABLE_LOG_DIR_NAME, STACKABLE_LOG_DIR) + .context(AddVolumeMountSnafu)? + .resources(merged_config.resources().clone().into()); + + // Use kcat sidecar for probing container status rather than the official Kafka tools, since they incur a lot of + // unacceptable perf overhead + cb_kcat_prober + .image_from_product_image(resolved_product_image) + .command(vec!["sleep".to_string(), "infinity".to_string()]) + .add_env_vars(vec![EnvVar { + name: "POD_NAME".to_string(), + value_from: Some(EnvVarSource { + field_ref: Some(ObjectFieldSelector { + api_version: Some("v1".to_string()), + field_path: "metadata.name".to_string(), + }), + ..EnvVarSource::default() + }), + ..EnvVar::default() + }]) + .resources( + ResourceRequirementsBuilder::new() + .with_cpu_request("100m") + .with_cpu_limit("200m") + .with_memory_request("128Mi") + .with_memory_limit("128Mi") + .build(), + ) + .add_volume_mount( + LISTENER_BOOTSTRAP_VOLUME_NAME, + STACKABLE_LISTENER_BOOTSTRAP_DIR, + ) + .context(AddVolumeMountSnafu)? + .add_volume_mount(LISTENER_BROKER_VOLUME_NAME, STACKABLE_LISTENER_BROKER_DIR) + .context(AddVolumeMountSnafu)? + // Only allow the global load balancing service to send traffic to pods that are members of the quorum + // This also acts as a hint to the StatefulSet controller to wait for each pod to enter quorum before taking down the next + .readiness_probe(Probe { + exec: Some(ExecAction { + // If the broker is able to get its fellow cluster members then it has at least completed basic registration at some point + command: Some(kcat_prober_container_commands(kafka_security)), + }), + timeout_seconds: Some(5), + period_seconds: Some(2), + ..Probe::default() + }); + + add_log_config_volume( + &mut pod_builder, + &validated_rg.config.logging, + &resource_names, + )?; + + let metadata = ObjectMetaBuilder::new() + .with_labels(recommended_labels.clone()) + .build(); + + if let Some(listener_class) = merged_config.listener_class() { + pod_builder + .add_listener_volume_by_listener_class( + LISTENER_BROKER_VOLUME_NAME, + listener_class.as_ref(), + &recommended_labels, + ) + .context(AddListenerVolumeSnafu)?; + } + + if let Some(broker_id_config_map_name) = &validated_cluster + .cluster_config + .broker_id_pod_config_map_name + { + pod_builder + .add_volume( + VolumeBuilder::new(BROKER_ID_POD_MAP_DIR_NAME) + .with_config_map(broker_id_config_map_name) + .build(), + ) + .context(AddVolumeSnafu)?; + cb_kafka + .add_volume_mount(BROKER_ID_POD_MAP_DIR_NAME, BROKER_ID_POD_MAP_DIR) + .context(AddVolumeMountSnafu)?; + } + + pod_builder + .metadata(metadata) + .image_pull_secrets_from_product_image(resolved_product_image) + .add_container(cb_kafka.build()) + .add_container(cb_kcat_prober.build()) + .affinity(&merged_config.affinity); + + add_common_pod_config(&mut pod_builder, &resource_names, service_account)?; + + add_vector_container( + &mut pod_builder, + &validated_rg.config.logging, + resolved_product_image, + &resource_names, + ); + + add_graceful_shutdown_config(merged_config, &mut pod_builder).context(GracefulShutdownSnafu)?; + + let mut pod_template = pod_builder.build_template(); + + let pod_template_spec = pod_template.spec.get_or_insert_with(PodSpec::default); + // Don't run kcat pod as PID 1, to ensure that default signal handlers apply + pod_template_spec.share_process_namespace = Some(true); + + // Pod overrides were already merged (role <- role group) during validation. + pod_template.merge_from(validated_rg.pod_overrides.clone()); + + Ok(StatefulSet { + metadata: ObjectMetaBuilder::new() + .name_and_namespace(validated_cluster) + .name(resource_names.stateful_set_name().to_string()) + .ownerreference(ownerreference_from_resource( + validated_cluster, + None, + Some(true), + )) + .with_labels(recommended_labels.clone()) + .with_label(RESTART_CONTROLLER_ENABLED_LABEL.to_owned()) + .build(), + spec: Some(StatefulSetSpec { + pod_management_policy: Some(POD_MANAGEMENT_POLICY_PARALLEL.to_string()), + replicas: validated_rg.replicas.map(i32::from), + selector: LabelSelector { + match_labels: Some( + validated_cluster + .role_group_selector(kafka_role, role_group_name) + .into(), + ), + ..LabelSelector::default() + }, + service_name: Some(resource_names.headless_service_name().to_string()), + template: pod_template, + volume_claim_templates: Some(pvcs), + ..StatefulSetSpec::default() + }), + status: None, + }) +} + +/// The controller rolegroup [`StatefulSet`] runs the rolegroup, as configured by the administrator. +pub fn build_controller_rolegroup_statefulset( + kafka_role: &KafkaRole, + role_group_name: &RoleGroupName, + validated_cluster: &ValidatedCluster, + validated_rg: &ValidatedRoleGroupConfig, + service_account: &ServiceAccount, +) -> Result { + let kafka_security = &validated_cluster.cluster_config.kafka_security; + let resolved_product_image = &validated_cluster.image; + let merged_config = &validated_rg.config.config; + let resource_names = validated_cluster.resource_names(kafka_role, role_group_name); + let recommended_labels = validated_cluster.recommended_labels(kafka_role, role_group_name); + + let kafka_container_name = ControllerContainer::Kafka.to_string(); + let mut cb_kafka = + ContainerBuilder::new(&kafka_container_name).context(InvalidContainerNameSnafu { + name: kafka_container_name.clone(), + })?; + + let mut pod_builder = PodBuilder::new(); + + let mut env = Vec::::from(validated_rg.env_overrides.clone()); + + env.push(EnvVar { + name: "NAMESPACE".to_string(), + value_from: Some(EnvVarSource { + field_ref: Some(ObjectFieldSelector { + api_version: Some("v1".to_string()), + field_path: "metadata.namespace".to_string(), + }), + ..EnvVarSource::default() + }), + ..EnvVar::default() + }); + + env.push(EnvVar { + name: "POD_NAME".to_string(), + value_from: Some(EnvVarSource { + field_ref: Some(ObjectFieldSelector { + api_version: Some("v1".to_string()), + field_path: "metadata.name".to_string(), + }), + ..EnvVarSource::default() + }), + ..EnvVar::default() + }); + + env.push(EnvVar { + name: "ROLEGROUP_HEADLESS_SERVICE_NAME".to_string(), + value: Some(resource_names.headless_service_name().to_string()), + ..EnvVar::default() + }); + + env.push(EnvVar { + name: "CLUSTER_DOMAIN".to_string(), + value: Some(validated_cluster.cluster_domain.to_string()), + ..EnvVar::default() + }); + + env.push(EnvVar { + name: "KAFKA_CLIENT_PORT".to_string(), + value: Some(kafka_security.client_port().to_string()), + ..EnvVar::default() + }); + + // Controllers need the ZooKeeper connection string for migration + if let Some(zookeeper_config_map_name) = + &validated_cluster.cluster_config.zookeeper_config_map_name + { + env.push(EnvVar { + name: ZOOKEEPER_ENV_VAR_NAME.to_string(), + value_from: Some(EnvVarSource { + config_map_key_ref: Some(ConfigMapKeySelector { + name: zookeeper_config_map_name.to_string(), + key: ZOOKEEPER_ENV_VAR_NAME.to_string(), + ..ConfigMapKeySelector::default() + }), + ..EnvVarSource::default() + }), + ..EnvVar::default() + }) + }; + + cb_kafka + .image_from_product_image(resolved_product_image) + .command(vec![ + "/bin/bash".to_string(), + "-x".to_string(), + "-euo".to_string(), + "pipefail".to_string(), + "-c".to_string(), + ]) + .args(vec![controller_kafka_container_command( + validated_cluster + .pod_descriptors(Some(kafka_role)) + .context(BuildPodDescriptorsSnafu)?, + &resolved_product_image.product_version, + )]) + .add_env_var("PRE_STOP_CONTROLLER_SLEEP_SECONDS", "10"); + + add_common_kafka_env( + &mut cb_kafka, + merged_config, + &validated_rg + .product_specific_common_config + .jvm_argument_overrides, + resolved_product_image, + kafka_role, + role_group_name, + )?; + + cb_kafka + .add_env_vars(env) + .add_container_ports(container_ports(kafka_security)) + .add_volume_mount(LOG_DIRS_VOLUME_NAME, STACKABLE_DATA_DIR) + .context(AddVolumeMountSnafu)? + .add_volume_mount(STACKABLE_CONFIG_DIR_NAME, STACKABLE_CONFIG_DIR) + .context(AddVolumeMountSnafu)? + .add_volume_mount(STACKABLE_LOG_CONFIG_DIR_NAME, STACKABLE_LOG_CONFIG_DIR) + .context(AddVolumeMountSnafu)? + .add_volume_mount(STACKABLE_LOG_DIR_NAME, STACKABLE_LOG_DIR) + .context(AddVolumeMountSnafu)? + .resources(merged_config.resources().clone().into()) + // TODO: improve probes + .liveness_probe(Probe { + tcp_socket: Some(TCPSocketAction { + port: IntOrString::Int(kafka_security.client_port().into()), + ..Default::default() + }), + timeout_seconds: Some(10), + period_seconds: Some(10), + failure_threshold: Some(6), + ..Probe::default() + }) + .readiness_probe(Probe { + tcp_socket: Some(TCPSocketAction { + port: IntOrString::Int(kafka_security.client_port().into()), + ..Default::default() + }), + timeout_seconds: Some(10), + period_seconds: Some(10), + failure_threshold: Some(6), + ..Probe::default() + }); + + add_log_config_volume( + &mut pod_builder, + &validated_rg.config.logging, + &resource_names, + )?; + + let metadata = ObjectMetaBuilder::new() + .with_labels(recommended_labels.clone()) + .build(); + + // Add TLS related volumes and volume mounts + let requested_secret_lifetime = merged_config + .deref() + .requested_secret_lifetime + .context(MissingSecretLifetimeSnafu)?; + add_controller_volume_and_volume_mounts( + kafka_security, + &mut pod_builder, + &mut cb_kafka, + &requested_secret_lifetime, + ) + .context(AddVolumesAndVolumeMountsSnafu)?; + + let kafka_container = cb_kafka.build(); + + pod_builder + .metadata(metadata) + .image_pull_secrets_from_product_image(resolved_product_image) + .add_container(kafka_container) + .affinity(&merged_config.affinity); + + add_common_pod_config(&mut pod_builder, &resource_names, service_account)?; + + add_vector_container( + &mut pod_builder, + &validated_rg.config.logging, + resolved_product_image, + &resource_names, + ); + + add_graceful_shutdown_config(merged_config, &mut pod_builder).context(GracefulShutdownSnafu)?; + + let mut pod_template = pod_builder.build_template(); + + // Pod overrides were already merged (role <- role group) during validation. + pod_template.merge_from(validated_rg.pod_overrides.clone()); + + Ok(StatefulSet { + metadata: ObjectMetaBuilder::new() + .name_and_namespace(validated_cluster) + .name(resource_names.stateful_set_name().to_string()) + .ownerreference(ownerreference_from_resource( + validated_cluster, + None, + Some(true), + )) + .with_labels(recommended_labels.clone()) + .with_label(RESTART_CONTROLLER_ENABLED_LABEL.to_owned()) + .build(), + spec: Some(StatefulSetSpec { + pod_management_policy: Some(POD_MANAGEMENT_POLICY_PARALLEL.to_string()), + update_strategy: Some(StatefulSetUpdateStrategy { + type_: Some("RollingUpdate".to_string()), + ..StatefulSetUpdateStrategy::default() + }), + replicas: validated_rg.replicas.map(i32::from), + selector: LabelSelector { + match_labels: Some( + validated_cluster + .role_group_selector(kafka_role, role_group_name) + .into(), + ), + ..LabelSelector::default() + }, + service_name: Some(resource_names.headless_service_name().to_string()), + template: pod_template, + volume_claim_templates: Some(merged_config.resources().storage.build_pvcs()), + ..StatefulSetSpec::default() + }), + status: None, + }) +} + +/// We only expose client HTTP / HTTPS and Metrics ports. +fn container_ports(kafka_security: &ValidatedKafkaSecurity) -> Vec { + let mut ports = vec![ + ContainerPort { + name: Some(METRICS_PORT_NAME.to_string()), + container_port: METRICS_PORT.into(), + protocol: Some("TCP".to_string()), + ..ContainerPort::default() + }, + ContainerPort { + name: Some(kafka_security.client_port_name().to_string()), + container_port: kafka_security.client_port().into(), + protocol: Some("TCP".to_string()), + ..ContainerPort::default() + }, + ]; + if kafka_security.has_kerberos_enabled() { + ports.push(ContainerPort { + name: Some(kafka_security.bootstrap_port_name().to_string()), + container_port: kafka_security.bootstrap_port().into(), + protocol: Some("TCP".to_string()), + ..ContainerPort::default() + }); + } + ports +} + +/// Adds the env vars that the broker and controller Kafka containers share: the JVM +/// arguments, log options, the `containerdebug` log directory and the node-id offset. +fn add_common_kafka_env( + cb_kafka: &mut ContainerBuilder, + merged_config: &AnyConfig, + jvm_argument_overrides: &JvmArgumentOverrides, + resolved_product_image: &ResolvedProductImage, + kafka_role: &KafkaRole, + role_group_name: &RoleGroupName, +) -> Result<(), Error> { + cb_kafka + .add_env_var( + "EXTRA_ARGS", + crate::controller::build::jvm::construct_non_heap_jvm_args( + merged_config, + jvm_argument_overrides, + ) + .context(ConstructJvmArgumentsSnafu)?, + ) + .add_env_var( + KAFKA_HEAP_OPTS, + crate::controller::build::jvm::construct_heap_jvm_args( + merged_config, + jvm_argument_overrides, + ) + .context(ConstructJvmArgumentsSnafu)?, + ) + .add_env_var( + kafka_log_opts_env_var(), + kafka_log_opts(&resolved_product_image.product_version), + ) + // Needed for the `containerdebug` process to log it's tracing information to. + .add_env_var( + "CONTAINERDEBUG_LOG_DIRECTORY", + format!("{STACKABLE_LOG_DIR}/containerdebug"), + ) + .add_env_var( + KAFKA_NODE_ID_OFFSET, + node_id_hash32_offset(kafka_role, role_group_name.as_ref()).to_string(), + ); + Ok(()) +} + +/// Adds the `log-config` volume, sourced either from the user-supplied custom log config +/// `ConfigMap` or the rolegroup `ConfigMap` (which carries the operator-generated config). +/// Branches on the *validated* Kafka-container logging choice. +fn add_log_config_volume( + pod_builder: &mut PodBuilder, + logging: &ValidatedLogging, + resource_names: &ResourceNames, +) -> Result<(), Error> { + let config_map = match &logging.kafka_container { + ValidatedContainerLogConfigChoice::Custom(config_map_name) => config_map_name.to_string(), + ValidatedContainerLogConfigChoice::Automatic(_) => { + resource_names.role_group_config_map().to_string() + } + }; + pod_builder + .add_volume( + VolumeBuilder::new(STACKABLE_LOG_CONFIG_DIR_NAME) + .with_config_map(config_map) + .build(), + ) + .context(AddVolumeSnafu)?; + Ok(()) +} + +/// Adds the `config` volume, the `log` emptyDir, the service account and the pod security +/// context that the broker and controller pods share. +fn add_common_pod_config( + pod_builder: &mut PodBuilder, + resource_names: &ResourceNames, + service_account: &ServiceAccount, +) -> Result<(), Error> { + pod_builder + .add_volume(Volume { + name: STACKABLE_CONFIG_DIR_NAME.to_string(), + config_map: Some(ConfigMapVolumeSource { + name: resource_names.role_group_config_map().to_string(), + ..ConfigMapVolumeSource::default() + }), + ..Volume::default() + }) + .context(AddVolumeSnafu)? + .add_empty_dir_volume( + STACKABLE_LOG_DIR_NAME, + Some(product_logging::framework::calculate_log_volume_size_limit( + &[MAX_KAFKA_LOG_FILES_SIZE], + )), + ) + .context(AddVolumeSnafu)? + .service_account_name(service_account.name_any()) + .security_context(PodSecurityContextBuilder::new().fs_group(1000).build()); + Ok(()) +} + +/// Adds the Vector log-aggregation sidecar container, when the Vector agent is enabled. +/// +/// Whether Vector is enabled, the per-container log config and the (validated) aggregator +/// discovery `ConfigMap` name are resolved up-front in +/// [`ValidatedLogging`](crate::controller::validate::ValidatedLogging). The container mounts the +/// static `vector.yaml` from the `config` volume and is driven by the env vars the +/// [`vector_container`] sets. +fn add_vector_container( + pod_builder: &mut PodBuilder, + logging: &ValidatedLogging, + resolved_product_image: &ResolvedProductImage, + resource_names: &ResourceNames, +) { + // Add vector container after kafka container to keep the defaulting into kafka container + if let Some(vector_container_log_config) = &logging.vector_container { + pod_builder.add_container(vector_container( + &VECTOR_CONTAINER_NAME, + resolved_product_image, + vector_container_log_config, + resource_names, + &VECTOR_CONFIG_VOLUME_NAME, + &VECTOR_LOG_VOLUME_NAME, + EnvVarSet::new(), + )); + } +} diff --git a/rust/operator-binary/src/controller/build/security.rs b/rust/operator-binary/src/controller/build/security.rs new file mode 100644 index 00000000..9bfcb732 --- /dev/null +++ b/rust/operator-binary/src/controller/build/security.rs @@ -0,0 +1,967 @@ +//! Build-step helpers that turn a [`ValidatedKafkaSecurity`] into Kafka configuration settings, +//! client properties, container commands and TLS volumes/mounts. +//! +//! These consume the validated security inputs and produce build artifacts; they must not perform +//! any validation themselves. +use std::collections::BTreeMap; + +use snafu::{ResultExt, Snafu}; +use stackable_operator::{ + builder::{ + self, + pod::{ + PodBuilder, + container::ContainerBuilder, + volume::{SecretFormat, SecretOperatorVolumeSourceBuilder, VolumeBuilder}, + }, + }, + commons::secret_class::SecretClassVolumeProvisionParts, + crd::authentication::core, + k8s_openapi::api::core::v1::Volume, + shared::time::Duration, +}; + +use crate::{ + controller::security::ValidatedKafkaSecurity, + crd::{ + LISTENER_BOOTSTRAP_VOLUME_NAME, LISTENER_BROKER_VOLUME_NAME, STACKABLE_KERBEROS_KRB5_PATH, + STACKABLE_LISTENER_BROKER_DIR, + listener::{ + self, KafkaListenerName, KafkaListenerProtocol, node_address_cmd_env, node_port_cmd_env, + }, + role::KafkaRole, + }, +}; + +// - TLS internal +const INTER_BROKER_LISTENER_NAME: &str = "inter.broker.listener.name"; +// - TLS global +const KEYSTORE_P12_FILE_NAME: &str = "keystore.p12"; +const OPA_TLS_MOUNT_PATH: &str = "/stackable/tls-opa"; +// opa +const OPA_TLS_VOLUME_NAME: &str = "tls-opa"; +const SSL_STORE_PASSWORD: &str = ""; +const SSL_STORE_TYPE_PKCS12: &str = "PKCS12"; +const SSL_CLIENT_AUTH_REQUIRED: &str = "required"; +const SASL_MECHANISM_GSSAPI: &str = "GSSAPI"; +const STACKABLE_KCAT_BINARY: &str = "/stackable/kcat"; +const PROPERTY_SECURITY_PROTOCOL: &str = "security.protocol"; +const PROPERTY_SASL_ENABLED_MECHANISMS: &str = "sasl.enabled.mechanisms"; +const PROPERTY_SASL_KERBEROS_SERVICE_NAME: &str = "sasl.kerberos.service.name"; +const PROPERTY_SASL_INTER_BROKER_MECHANISM: &str = "sasl.mechanism.inter.broker.protocol"; +const STACKABLE_TLS_KAFKA_INTERNAL_DIR: &str = "/stackable/tls-kafka-internal"; +const STACKABLE_TLS_KAFKA_INTERNAL_VOLUME_NAME: &str = "tls-kafka-internal"; +const STACKABLE_TLS_KAFKA_SERVER_DIR: &str = "/stackable/tls-kafka-server"; +const STACKABLE_TLS_KAFKA_SERVER_VOLUME_NAME: &str = "tls-kafka-server"; +// directories +const STACKABLE_TLS_KCAT_DIR: &str = "/stackable/tls-kcat"; +const STACKABLE_TLS_KCAT_VOLUME_NAME: &str = "tls-kcat"; +const TRUSTSTORE_P12_FILE_NAME: &str = "truststore.p12"; + +#[derive(Snafu, Debug)] +pub enum Error { + #[snafu(display("failed to build the secret operator Volume"))] + SecretVolumeBuild { + source: stackable_operator::builder::pod::volume::SecretOperatorVolumeSourceBuilderError, + }, + + #[snafu(display("failed to add needed volume"))] + AddVolume { source: builder::pod::Error }, + + #[snafu(display("failed to add needed volumeMount"))] + AddVolumeMount { + source: builder::pod::container::Error, + }, + + #[snafu(display("failed to build OPA TLS certificate volume"))] + OpaTlsCertSecretClassVolumeBuild { + source: stackable_operator::builder::pod::volume::SecretOperatorVolumeSourceBuilderError, + }, +} + +pub fn copy_opa_tls_cert_command(security: &ValidatedKafkaSecurity) -> String { + match security.opa_secret_class().is_some() { + true => format!( + "keytool -importcert -file {opa_mount_path}/ca.crt -keystore {tls_dir}/{truststore} -storepass '{tls_password}' -alias opa-ca -noprompt", + opa_mount_path = OPA_TLS_MOUNT_PATH, + tls_dir = STACKABLE_TLS_KAFKA_INTERNAL_DIR, + truststore = TRUSTSTORE_P12_FILE_NAME, + tls_password = SSL_STORE_PASSWORD, + ), + false => "".to_string(), + } +} + +/// Returns the commands for the kcat readiness probe. +pub fn kcat_prober_container_commands(security: &ValidatedKafkaSecurity) -> Vec { + let mut args = vec![]; + let port = security.client_port(); + + if security.tls_client_authentication_class().is_some() { + args.push(STACKABLE_KCAT_BINARY.to_string()); + args.push("-b".to_string()); + args.push(format!("localhost:{}", port)); + args.extend(kcat_client_auth_ssl(STACKABLE_TLS_KCAT_DIR)); + args.push("-L".to_string()); + } else if security.has_kerberos_enabled() { + let service_name = KafkaRole::Broker.kerberos_service_name(); + // here we need to specify a shell so that variable substitution will work + // see e.g. https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1ExecAction.md + args.push("/bin/bash".to_string()); + args.push("-x".to_string()); + args.push("-euo".to_string()); + args.push("pipefail".to_string()); + args.push("-c".to_string()); + + // the entire command needs to be subject to the -c directive + // to prevent short-circuiting + let mut bash_args = vec![]; + bash_args.push( + format!( + "export KERBEROS_REALM=$(grep -oP 'default_realm = \\K.*' {});", + STACKABLE_KERBEROS_KRB5_PATH + ) + .to_string(), + ); + bash_args.push( + format!( + "export POD_BROKER_LISTENER_ADDRESS={};", + node_address_cmd_env(STACKABLE_LISTENER_BROKER_DIR) + ) + .to_string(), + ); + bash_args.push( + format!( + "export POD_BROKER_LISTENER_PORT={};", + node_port_cmd_env(STACKABLE_LISTENER_BROKER_DIR, security.client_port_name()) + ) + .to_string(), + ); + bash_args.push(STACKABLE_KCAT_BINARY.to_string()); + bash_args.push("-b".to_string()); + bash_args.push("$POD_BROKER_LISTENER_ADDRESS:$POD_BROKER_LISTENER_PORT".to_string()); + bash_args.extend(kcat_client_sasl_ssl(STACKABLE_TLS_KCAT_DIR, service_name)); + bash_args.push("-L".to_string()); + + args.push(bash_args.join(" ")); + } else if security.tls_server_secret_class().is_some() { + args.push(STACKABLE_KCAT_BINARY.to_string()); + args.push("-b".to_string()); + args.push(format!("localhost:{}", port)); + args.extend(kcat_client_ssl(STACKABLE_TLS_KCAT_DIR)); + args.push("-L".to_string()); + } else { + args.push(STACKABLE_KCAT_BINARY.to_string()); + args.push("-b".to_string()); + args.push(format!("localhost:{}", port)); + args.push("-L".to_string()); + } + + args +} + +/// Returns a configuration file that can be used by Kafka clients running inside the +/// Kubernetes cluster to connect to the Kafka servers. +pub fn client_properties(security: &ValidatedKafkaSecurity) -> Vec<(String, Option)> { + let mut props = vec![]; + + if security.tls_client_authentication_class().is_some() { + props.push(( + PROPERTY_SECURITY_PROTOCOL.to_string(), + Some(KafkaListenerProtocol::Ssl.to_string()), + )); + props.push(( + "ssl.client.auth".to_string(), + Some(SSL_CLIENT_AUTH_REQUIRED.to_string()), + )); + push_client_ssl_stores(&mut props, STACKABLE_TLS_KAFKA_SERVER_DIR); + } else if security.has_kerberos_enabled() { + // TODO: to make this configuration file usable out of the box the operator needs to be + // refactored to write out Java jaas files instead of passing command line parameters + // to the Kafka daemon scripts. + // This will simplify the code and the command lines lot. + // It will also make the jaas files reusable by the Kafka shell scripts. + props.push(( + PROPERTY_SECURITY_PROTOCOL.to_string(), + Some(KafkaListenerProtocol::SaslSsl.to_string()), + )); + push_client_ssl_stores(&mut props, STACKABLE_TLS_KAFKA_SERVER_DIR); + props.push(( + PROPERTY_SASL_ENABLED_MECHANISMS.to_string(), + Some(SASL_MECHANISM_GSSAPI.to_string()), + )); + props.push(( + PROPERTY_SASL_KERBEROS_SERVICE_NAME.to_string(), + Some(KafkaRole::Broker.kerberos_service_name().to_string()), + )); + props.push(( + PROPERTY_SASL_INTER_BROKER_MECHANISM.to_string(), + Some(SASL_MECHANISM_GSSAPI.to_string()), + )); + props.push(( + "sasl.jaas.config".to_string(), + Some(format!("com.sun.security.auth.module.Krb5LoginModule required useKeyTab=true storeKey=true keyTab=\"{keytab}\" principal=\"{service}/{pod}@{realm}\"", + keytab="/stackable/kerberos/keytab", + service=KafkaRole::Broker.kerberos_service_name(), + pod="todo", + realm="$KERBEROS_REALM")))); + } else if security.tls_server_secret_class().is_some() { + props.push(( + PROPERTY_SECURITY_PROTOCOL.to_string(), + Some(KafkaListenerProtocol::Ssl.to_string()), + )); + push_client_ssl_truststore(&mut props, STACKABLE_TLS_KAFKA_SERVER_DIR); + } else { + props.push(( + PROPERTY_SECURITY_PROTOCOL.to_string(), + Some(KafkaListenerProtocol::Plaintext.to_string()), + )); + } + + props +} + +/// Adds required volumes and volume mounts to the broker pod and container builders +/// depending on the tls and authentication settings. +pub fn add_broker_volume_and_volume_mounts( + security: &ValidatedKafkaSecurity, + pod_builder: &mut PodBuilder, + cb_kcat_prober: &mut ContainerBuilder, + cb_kafka: &mut ContainerBuilder, + requested_secret_lifetime: &Duration, +) -> Result<(), Error> { + // add tls (server or client authentication volumes) if required + if let Some(tls_server_secret_class) = tls_secret_class(security) { + // We have to mount tls pem files for kcat (the mount can be used directly) + pod_builder + .add_volume(create_kcat_tls_volume( + STACKABLE_TLS_KCAT_VOLUME_NAME, + tls_server_secret_class, + requested_secret_lifetime, + )?) + .context(AddVolumeSnafu)?; + cb_kcat_prober + .add_volume_mount(STACKABLE_TLS_KCAT_VOLUME_NAME, STACKABLE_TLS_KCAT_DIR) + .context(AddVolumeMountSnafu)?; + // Keystores fore the kafka container + pod_builder + .add_volume(create_tls_keystore_volume( + STACKABLE_TLS_KAFKA_SERVER_VOLUME_NAME, + tls_server_secret_class, + requested_secret_lifetime, + )?) + .context(AddVolumeSnafu)?; + cb_kafka + .add_volume_mount( + STACKABLE_TLS_KAFKA_SERVER_VOLUME_NAME, + STACKABLE_TLS_KAFKA_SERVER_DIR, + ) + .context(AddVolumeMountSnafu)?; + } + + if let Some(tls_internal_secret_class) = security.tls_internal_secret_class() { + pod_builder + .add_volume(create_tls_keystore_volume( + STACKABLE_TLS_KAFKA_INTERNAL_VOLUME_NAME, + tls_internal_secret_class, + requested_secret_lifetime, + )?) + .context(AddVolumeSnafu)?; + cb_kafka + .add_volume_mount( + STACKABLE_TLS_KAFKA_INTERNAL_VOLUME_NAME, + STACKABLE_TLS_KAFKA_INTERNAL_DIR, + ) + .context(AddVolumeMountSnafu)?; + } + + if let Some(secret_class) = security.opa_secret_class() { + cb_kafka + .add_volume_mount(OPA_TLS_VOLUME_NAME, OPA_TLS_MOUNT_PATH) + .context(AddVolumeMountSnafu)?; + + pod_builder + .add_volume( + VolumeBuilder::new(OPA_TLS_VOLUME_NAME) + .ephemeral( + SecretOperatorVolumeSourceBuilder::new( + secret_class, + // Only the truststore is required to connect to OPA. + SecretClassVolumeProvisionParts::Public, + ) + .build() + .context(OpaTlsCertSecretClassVolumeBuildSnafu)?, + ) + .build(), + ) + .context(AddVolumeSnafu)?; + } + + Ok(()) +} + +/// Adds required volumes and volume mounts to the controller pod and container builders +/// depending on the tls and authentication settings. +pub fn add_controller_volume_and_volume_mounts( + security: &ValidatedKafkaSecurity, + pod_builder: &mut PodBuilder, + cb_kafka: &mut ContainerBuilder, + requested_secret_lifetime: &Duration, +) -> Result<(), Error> { + if let Some(tls_internal_secret_class) = security.tls_internal_secret_class() { + pod_builder + .add_volume( + VolumeBuilder::new(STACKABLE_TLS_KAFKA_INTERNAL_VOLUME_NAME) + .ephemeral( + SecretOperatorVolumeSourceBuilder::new( + tls_internal_secret_class, + // Kafka needs both the public certificate and the private key for + // the internal communication. + SecretClassVolumeProvisionParts::PublicPrivate, + ) + .with_pod_scope() + .with_format(SecretFormat::TlsPkcs12) + .with_auto_tls_cert_lifetime(*requested_secret_lifetime) + .build() + .context(SecretVolumeBuildSnafu)?, + ) + .build(), + ) + .context(AddVolumeSnafu)?; + cb_kafka + .add_volume_mount( + STACKABLE_TLS_KAFKA_INTERNAL_VOLUME_NAME, + STACKABLE_TLS_KAFKA_INTERNAL_DIR, + ) + .context(AddVolumeMountSnafu)?; + } + + Ok(()) +} + +/// Inserts the `listener..ssl.{keystore,truststore}.{location,password,type}` +/// settings for `listener`, pointing at the PKCS12 stores under `dir`. +fn insert_listener_ssl_stores( + config: &mut BTreeMap, + listener: &KafkaListenerName, + dir: &str, +) { + config.insert( + listener.listener_ssl_keystore_location(), + format!("{dir}/{}", KEYSTORE_P12_FILE_NAME), + ); + config.insert( + listener.listener_ssl_keystore_password(), + SSL_STORE_PASSWORD.to_string(), + ); + config.insert( + listener.listener_ssl_keystore_type(), + SSL_STORE_TYPE_PKCS12.to_string(), + ); + config.insert( + listener.listener_ssl_truststore_location(), + format!("{dir}/{}", TRUSTSTORE_P12_FILE_NAME), + ); + config.insert( + listener.listener_ssl_truststore_password(), + SSL_STORE_PASSWORD.to_string(), + ); + config.insert( + listener.listener_ssl_truststore_type(), + SSL_STORE_TYPE_PKCS12.to_string(), + ); +} + +/// Pushes the client-side `ssl.keystore.*` and `ssl.truststore.*` properties, pointing +/// at the PKCS12 stores under `dir`. +fn push_client_ssl_stores(props: &mut Vec<(String, Option)>, dir: &str) { + props.push(( + "ssl.keystore.type".to_string(), + Some(SSL_STORE_TYPE_PKCS12.to_string()), + )); + props.push(( + "ssl.keystore.location".to_string(), + Some(format!("{dir}/{}", KEYSTORE_P12_FILE_NAME)), + )); + props.push(( + "ssl.keystore.password".to_string(), + Some(SSL_STORE_PASSWORD.to_string()), + )); + push_client_ssl_truststore(props, dir); +} + +/// Pushes the client-side `ssl.truststore.*` properties, pointing at the PKCS12 +/// truststore under `dir`. +fn push_client_ssl_truststore(props: &mut Vec<(String, Option)>, dir: &str) { + props.push(( + "ssl.truststore.type".to_string(), + Some(SSL_STORE_TYPE_PKCS12.to_string()), + )); + props.push(( + "ssl.truststore.location".to_string(), + Some(format!("{dir}/{}", TRUSTSTORE_P12_FILE_NAME)), + )); + props.push(( + "ssl.truststore.password".to_string(), + Some(SSL_STORE_PASSWORD.to_string()), + )); +} + +/// Returns required Kafka configuration settings for the `broker.properties` file +/// depending on the tls and authentication settings. +pub fn broker_config_settings(security: &ValidatedKafkaSecurity) -> BTreeMap { + let mut config = BTreeMap::new(); + + // We set either client tls with authentication or client tls without authentication + // If authentication is explicitly required we do not want to have any other CAs to + // be trusted. + if security.tls_client_authentication_class().is_some() + || security.tls_server_secret_class().is_some() + { + insert_listener_ssl_stores( + &mut config, + &KafkaListenerName::Client, + STACKABLE_TLS_KAFKA_SERVER_DIR, + ); + if security.tls_client_authentication_class().is_some() { + // client auth required + config.insert( + KafkaListenerName::Client.listener_ssl_client_auth(), + SSL_CLIENT_AUTH_REQUIRED.to_string(), + ); + } + } + + if security.has_kerberos_enabled() { + // Bootstrap + insert_listener_ssl_stores( + &mut config, + &KafkaListenerName::Bootstrap, + STACKABLE_TLS_KAFKA_SERVER_DIR, + ); + config.insert( + PROPERTY_SASL_ENABLED_MECHANISMS.to_string(), + SASL_MECHANISM_GSSAPI.to_string(), + ); + config.insert( + PROPERTY_SASL_KERBEROS_SERVICE_NAME.to_string(), + KafkaRole::Broker.kerberos_service_name().to_string(), + ); + config.insert( + PROPERTY_SASL_INTER_BROKER_MECHANISM.to_string(), + SASL_MECHANISM_GSSAPI.to_string(), + ); + tracing::debug!("Kerberos configs added: [{:#?}]", config); + } + + // Internal TLS + if security.tls_internal_secret_class().is_some() { + // BROKERS + insert_listener_ssl_stores( + &mut config, + &KafkaListenerName::Internal, + STACKABLE_TLS_KAFKA_INTERNAL_DIR, + ); + // CONTROLLERS + insert_listener_ssl_stores( + &mut config, + &KafkaListenerName::Controller, + STACKABLE_TLS_KAFKA_INTERNAL_DIR, + ); + // client auth required + config.insert( + KafkaListenerName::Internal.listener_ssl_client_auth(), + SSL_CLIENT_AUTH_REQUIRED.to_string(), + ); + } + + //OPA Tls + if security.opa_secret_class().is_some() { + config.insert( + "opa.authorizer.truststore.path".to_string(), + format!( + "{}/{}", + STACKABLE_TLS_KAFKA_INTERNAL_DIR, TRUSTSTORE_P12_FILE_NAME + ), + ); + config.insert( + "opa.authorizer.truststore.password".to_string(), + SSL_STORE_PASSWORD.to_string(), + ); + config.insert( + "opa.authorizer.truststore.type".to_string(), + SSL_STORE_TYPE_PKCS12.to_string(), + ); + } + + // common + config.insert( + INTER_BROKER_LISTENER_NAME.to_string(), + listener::KafkaListenerName::Internal.to_string(), + ); + + config +} + +/// Returns required Kafka configuration settings for the `controller.properties` file +/// depending on the tls and authentication settings. +pub fn controller_config_settings(security: &ValidatedKafkaSecurity) -> BTreeMap { + let mut config = BTreeMap::new(); + + if security.tls_client_authentication_class().is_some() + || security.tls_internal_secret_class().is_some() + { + insert_listener_ssl_stores( + &mut config, + &KafkaListenerName::Controller, + STACKABLE_TLS_KAFKA_INTERNAL_DIR, + ); + + // The TLS properties for the internal broker listener are needed by the Kraft controllers + // too during metadata migration from ZooKeeper to Kraft mode. + insert_listener_ssl_stores( + &mut config, + &KafkaListenerName::Internal, + STACKABLE_TLS_KAFKA_INTERNAL_DIR, + ); + // We set either client tls with authentication or client tls without authentication + // If authentication is explicitly required we do not want to have any other CAs to + // be trusted. + if security.tls_client_authentication_class().is_some() { + // client auth required + config.insert( + KafkaListenerName::Controller.listener_ssl_client_auth(), + SSL_CLIENT_AUTH_REQUIRED.to_string(), + ); + } + } + + // Kerberos + if security.has_kerberos_enabled() { + config.insert( + PROPERTY_SASL_ENABLED_MECHANISMS.to_string(), + SASL_MECHANISM_GSSAPI.to_string(), + ); + config.insert( + PROPERTY_SASL_KERBEROS_SERVICE_NAME.to_string(), + KafkaRole::Controller.kerberos_service_name().to_string(), + ); + config.insert( + PROPERTY_SASL_INTER_BROKER_MECHANISM.to_string(), + SASL_MECHANISM_GSSAPI.to_string(), + ); + tracing::debug!("Kerberos configs added: [{:#?}]", config); + } + + config +} + +/// Returns the `SecretClass` provided in a `AuthenticationClass` for TLS. +fn tls_secret_class(security: &ValidatedKafkaSecurity) -> Option<&str> { + security + .tls_client_authentication_class() + .and_then(|auth_class| match &auth_class.spec.provider { + core::v1alpha1::AuthenticationClassProvider::Tls(tls) => { + tls.client_cert_secret_class.as_deref() + } + _ => None, + }) + .or_else(|| security.tls_server_secret_class()) +} + +/// Creates ephemeral volumes to mount the `SecretClass` into the Pods for kcat client +fn create_kcat_tls_volume( + volume_name: &str, + secret_class_name: &str, + requested_secret_lifetime: &Duration, +) -> Result { + Ok(VolumeBuilder::new(volume_name) + .ephemeral( + SecretOperatorVolumeSourceBuilder::new( + secret_class_name, + // Both the public certificate and the private key are required for the kcat + // client authentication. + SecretClassVolumeProvisionParts::PublicPrivate, + ) + .with_pod_scope() + .with_format(SecretFormat::TlsPem) + .with_auto_tls_cert_lifetime(*requested_secret_lifetime) + .build() + .context(SecretVolumeBuildSnafu)?, + ) + .build()) +} + +/// Creates ephemeral volumes to mount the `SecretClass` into the Pods as keystores +fn create_tls_keystore_volume( + volume_name: &str, + secret_class_name: &str, + requested_secret_lifetime: &Duration, +) -> Result { + Ok(VolumeBuilder::new(volume_name) + .ephemeral( + SecretOperatorVolumeSourceBuilder::new( + secret_class_name, + // Both the keystore and truststore are required for keystore volume. + SecretClassVolumeProvisionParts::PublicPrivate, + ) + .with_pod_scope() + .with_listener_volume_scope(LISTENER_BROKER_VOLUME_NAME) + .with_listener_volume_scope(LISTENER_BOOTSTRAP_VOLUME_NAME) + .with_format(SecretFormat::TlsPkcs12) + .with_auto_tls_cert_lifetime(*requested_secret_lifetime) + .build() + .context(SecretVolumeBuildSnafu)?, + ) + .build()) +} + +fn kcat_client_auth_ssl(cert_directory: &str) -> Vec { + vec![ + "-X".to_string(), + "security.protocol=SSL".to_string(), + "-X".to_string(), + format!("ssl.key.location={cert_directory}/tls.key"), + "-X".to_string(), + format!("ssl.certificate.location={cert_directory}/tls.crt"), + "-X".to_string(), + format!("ssl.ca.location={cert_directory}/ca.crt"), + ] +} + +fn kcat_client_ssl(cert_directory: &str) -> Vec { + vec![ + "-X".to_string(), + "security.protocol=SSL".to_string(), + "-X".to_string(), + format!("ssl.ca.location={cert_directory}/ca.crt"), + ] +} + +fn kcat_client_sasl_ssl(cert_directory: &str, service_name: &str) -> Vec { + vec![ + "-X".to_string(), + "security.protocol=SASL_SSL".to_string(), + "-X".to_string(), + format!("ssl.ca.location={cert_directory}/ca.crt"), + "-X".to_string(), + "sasl.kerberos.keytab=/stackable/kerberos/keytab".to_string(), + "-X".to_string(), + format!("sasl.mechanism={SASL_MECHANISM_GSSAPI}"), + "-X".to_string(), + format!("sasl.kerberos.service.name={service_name}"), + "-X".to_string(), + format!( + "sasl.kerberos.principal={service_name}/$POD_BROKER_LISTENER_ADDRESS@$KERBEROS_REALM" + ), + ] +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use stackable_operator::{ + builder::meta::ObjectMetaBuilder, + crd::authentication::{core, kerberos, tls}, + }; + + use super::*; + use crate::crd::authentication::ResolvedAuthenticationClasses; + + fn tls_auth_class() -> core::v1alpha1::AuthenticationClass { + core::v1alpha1::AuthenticationClass { + metadata: ObjectMetaBuilder::new().name("tls-auth").build(), + spec: core::v1alpha1::AuthenticationClassSpec { + provider: core::v1alpha1::AuthenticationClassProvider::Tls( + tls::v1alpha1::AuthenticationProvider { + client_cert_secret_class: Some("client-auth-secret-class".to_string()), + }, + ), + }, + } + } + + fn kerberos_auth_class() -> core::v1alpha1::AuthenticationClass { + core::v1alpha1::AuthenticationClass { + metadata: ObjectMetaBuilder::new().name("kerberos-auth").build(), + spec: core::v1alpha1::AuthenticationClassSpec { + provider: core::v1alpha1::AuthenticationClassProvider::Kerberos( + kerberos::v1alpha1::AuthenticationProvider { + kerberos_secret_class: "kerberos-secret-class".to_string(), + }, + ), + }, + } + } + + fn no_auth() -> ResolvedAuthenticationClasses { + ResolvedAuthenticationClasses::new(vec![]) + } + + /// Plaintext: no TLS, no authentication, no OPA. + fn plaintext() -> ValidatedKafkaSecurity { + ValidatedKafkaSecurity::new(no_auth(), None, None, None) + } + + /// Server TLS only (encryption without client authentication). + fn server_tls() -> ValidatedKafkaSecurity { + ValidatedKafkaSecurity::new(no_auth(), None, Some("tls".parse().unwrap()), None) + } + + /// Mutual TLS (client-certificate authentication). + fn client_auth_tls() -> ValidatedKafkaSecurity { + ValidatedKafkaSecurity::new( + ResolvedAuthenticationClasses::new(vec![tls_auth_class()]), + None, + Some("tls".parse().unwrap()), + None, + ) + } + + /// Kerberos, which also requires server and internal TLS. + fn kerberos() -> ValidatedKafkaSecurity { + ValidatedKafkaSecurity::new( + ResolvedAuthenticationClasses::new(vec![kerberos_auth_class()]), + Some("tls".parse().unwrap()), + Some("tls".parse().unwrap()), + None, + ) + } + + /// Internal TLS only (broker/controller encryption without client TLS). + fn internal_tls() -> ValidatedKafkaSecurity { + ValidatedKafkaSecurity::new(no_auth(), Some("tls".parse().unwrap()), None, None) + } + + /// OPA authorization with a TLS truststore (internal + server TLS also enabled). + fn opa() -> ValidatedKafkaSecurity { + ValidatedKafkaSecurity::new( + no_auth(), + Some("tls".parse().unwrap()), + Some("tls".parse().unwrap()), + Some("opa-tls".parse().unwrap()), + ) + } + + fn as_str_vec(commands: &[String]) -> Vec<&str> { + commands.iter().map(String::as_str).collect() + } + + fn as_map(props: Vec<(String, Option)>) -> BTreeMap> { + props.into_iter().collect() + } + + // ---- kcat_prober_container_commands ---- + + #[test] + fn kcat_prober_plaintext_targets_insecure_client_port() { + let commands = kcat_prober_container_commands(&plaintext()); + assert_eq!( + as_str_vec(&commands), + vec!["/stackable/kcat", "-b", "localhost:9092", "-L"] + ); + } + + #[test] + fn kcat_prober_server_tls_trusts_ca_without_client_key() { + let commands = kcat_prober_container_commands(&server_tls()); + let commands = as_str_vec(&commands); + + assert_eq!(commands[0], "/stackable/kcat"); + assert_eq!(commands[2], "localhost:9093"); + assert_eq!(commands[commands.len() - 1], "-L"); + assert!( + commands + .iter() + .any(|a| a.contains("ssl.ca.location=/stackable/tls-kcat/ca.crt")) + ); + assert!(!commands.iter().any(|a| a.contains("ssl.key.location"))); + } + + #[test] + fn kcat_prober_client_auth_presents_client_key() { + let commands = kcat_prober_container_commands(&client_auth_tls()); + let commands = as_str_vec(&commands); + + assert_eq!(commands[0], "/stackable/kcat"); + assert_eq!(commands[2], "localhost:9093"); + assert!( + commands + .iter() + .any(|a| a.contains("ssl.key.location=/stackable/tls-kcat/tls.key")) + ); + } + + #[test] + fn kcat_prober_kerberos_wraps_command_in_bash() { + let commands = kcat_prober_container_commands(&kerberos()); + let commands = as_str_vec(&commands); + + assert_eq!(commands[0], "/bin/bash"); + assert_eq!(commands[4], "-c"); + assert_eq!(commands.len(), 6); + + let script = commands[5]; + assert!(script.contains("KERBEROS_REALM")); + assert!(script.contains("/stackable/kcat")); + assert!(script.contains("sasl.mechanism=GSSAPI")); + assert!(script.contains("sasl.kerberos.service.name=kafka")); + } + + // ---- copy_opa_tls_cert_command ---- + + #[test] + fn copy_opa_tls_cert_command_imports_ca_when_opa_enabled() { + let command = copy_opa_tls_cert_command(&opa()); + assert!(command.contains("keytool -importcert")); + assert!(command.contains("-alias opa-ca")); + assert!(command.contains("/stackable/tls-opa/ca.crt")); + } + + #[test] + fn copy_opa_tls_cert_command_empty_without_opa() { + assert_eq!(copy_opa_tls_cert_command(&plaintext()), ""); + } + + // ---- client_properties ---- + + #[test] + fn client_properties_plaintext() { + let props = as_map(client_properties(&plaintext())); + assert_eq!( + props.get("security.protocol"), + Some(&Some("PLAINTEXT".to_string())) + ); + assert_eq!(props.len(), 1); + } + + #[test] + fn client_properties_server_tls_uses_truststore_only() { + let props = as_map(client_properties(&server_tls())); + assert_eq!( + props.get("security.protocol"), + Some(&Some("SSL".to_string())) + ); + assert!(props.contains_key("ssl.truststore.location")); + assert!(!props.contains_key("ssl.keystore.location")); + } + + #[test] + fn client_properties_client_auth_requires_keystore_and_client_auth() { + let props = as_map(client_properties(&client_auth_tls())); + assert_eq!( + props.get("security.protocol"), + Some(&Some("SSL".to_string())) + ); + assert_eq!( + props.get("ssl.client.auth"), + Some(&Some("required".to_string())) + ); + assert!(props.contains_key("ssl.keystore.location")); + } + + #[test] + fn client_properties_kerberos_uses_sasl_ssl() { + let props = as_map(client_properties(&kerberos())); + assert_eq!( + props.get("security.protocol"), + Some(&Some("SASL_SSL".to_string())) + ); + assert_eq!( + props.get("sasl.enabled.mechanisms"), + Some(&Some("GSSAPI".to_string())) + ); + assert_eq!( + props.get("sasl.kerberos.service.name"), + Some(&Some("kafka".to_string())) + ); + assert!(props.contains_key("sasl.jaas.config")); + } + + // ---- broker_config_settings ---- + + #[test] + fn broker_config_plaintext_only_sets_inter_broker_listener() { + let config = broker_config_settings(&plaintext()); + assert_eq!( + config.get("inter.broker.listener.name"), + Some(&"INTERNAL".to_string()) + ); + assert_eq!(config.len(), 1); + } + + #[test] + fn broker_config_client_auth_requires_client_auth() { + let config = broker_config_settings(&client_auth_tls()); + assert_eq!( + config.get("listener.name.client.ssl.client.auth"), + Some(&"required".to_string()) + ); + assert!(config.contains_key("listener.name.client.ssl.keystore.location")); + } + + #[test] + fn broker_config_kerberos_adds_sasl_and_bootstrap_stores() { + let config = broker_config_settings(&kerberos()); + assert_eq!( + config.get("sasl.enabled.mechanisms"), + Some(&"GSSAPI".to_string()) + ); + assert_eq!( + config.get("sasl.kerberos.service.name"), + Some(&"kafka".to_string()) + ); + assert_eq!( + config.get("sasl.mechanism.inter.broker.protocol"), + Some(&"GSSAPI".to_string()) + ); + assert!(config.contains_key("listener.name.bootstrap.ssl.keystore.location")); + } + + #[test] + fn broker_config_internal_tls_covers_internal_and_controller() { + let config = broker_config_settings(&internal_tls()); + assert!(config.contains_key("listener.name.internal.ssl.keystore.location")); + assert!(config.contains_key("listener.name.controller.ssl.keystore.location")); + assert_eq!( + config.get("listener.name.internal.ssl.client.auth"), + Some(&"required".to_string()) + ); + } + + #[test] + fn broker_config_opa_adds_authorizer_truststore() { + let config = broker_config_settings(&opa()); + assert!(config.contains_key("opa.authorizer.truststore.path")); + assert!(config.contains_key("opa.authorizer.truststore.password")); + assert!(config.contains_key("opa.authorizer.truststore.type")); + } + + // ---- controller_config_settings ---- + + #[test] + fn controller_config_plaintext_is_empty() { + assert!(controller_config_settings(&plaintext()).is_empty()); + } + + #[test] + fn controller_config_internal_tls_covers_controller_and_internal() { + let config = controller_config_settings(&internal_tls()); + assert!(config.contains_key("listener.name.controller.ssl.keystore.location")); + assert!(config.contains_key("listener.name.internal.ssl.keystore.location")); + } + + #[test] + fn controller_config_kerberos_adds_sasl() { + let config = controller_config_settings(&kerberos()); + assert_eq!( + config.get("sasl.enabled.mechanisms"), + Some(&"GSSAPI".to_string()) + ); + assert_eq!( + config.get("sasl.kerberos.service.name"), + Some(&"kafka".to_string()) + ); + } +} diff --git a/rust/operator-binary/src/controller/dereference.rs b/rust/operator-binary/src/controller/dereference.rs index b7a22107..c90d1258 100644 --- a/rust/operator-binary/src/controller/dereference.rs +++ b/rust/operator-binary/src/controller/dereference.rs @@ -9,7 +9,7 @@ //! and stays here as-is. use snafu::{ResultExt, Snafu}; -use stackable_operator::client::Client; +use stackable_operator::{client::Client, utils::cluster_info::KubernetesClusterInfo}; use crate::crd::{ authentication::{self, ResolvedAuthenticationClasses}, @@ -33,6 +33,7 @@ type Result = std::result::Result; pub struct DereferencedObjects { pub authentication_classes: ResolvedAuthenticationClasses, pub authorization_config: Option, + pub kubernetes_cluster_info: KubernetesClusterInfo, } /// Fetches all Kubernetes objects referenced from the [`v1alpha1::KafkaCluster`] spec. @@ -59,5 +60,6 @@ pub async fn dereference( Ok(DereferencedObjects { authentication_classes, authorization_config, + kubernetes_cluster_info: client.kubernetes_cluster_info.clone(), }) } diff --git a/rust/operator-binary/src/config/node_id_hasher.rs b/rust/operator-binary/src/controller/node_id_hasher.rs similarity index 69% rename from rust/operator-binary/src/config/node_id_hasher.rs rename to rust/operator-binary/src/controller/node_id_hasher.rs index 51690a0e..a8ae5f3c 100644 --- a/rust/operator-binary/src/config/node_id_hasher.rs +++ b/rust/operator-binary/src/controller/node_id_hasher.rs @@ -1,6 +1,4 @@ -use stackable_operator::role_utils::RoleGroupRef; - -use crate::crd::v1alpha1::KafkaCluster; +use crate::crd::role::KafkaRole; /// The Kafka node.id needs to be unique across the Kafka cluster. /// This function generates an integer that is stable for a given role group @@ -8,12 +6,8 @@ use crate::crd::v1alpha1::KafkaCluster; /// This integer is then added to the pod index to compute the final node.id /// The node.id is only set and used in Kraft mode. /// Warning: this is not safe from collisions. -pub fn node_id_hash32_offset(rolegroup_ref: &RoleGroupRef) -> u32 { - let hash = fnv_hash32(&format!( - "{role}-{rolegroup}", - role = rolegroup_ref.role, - rolegroup = rolegroup_ref.role_group - )); +pub fn node_id_hash32_offset(role: &KafkaRole, role_group: &str) -> u32 { + let hash = fnv_hash32(&format!("{role}-{role_group}")); let range = hash & 0x0000FFFF; // Kafka uses signed integer range * 0x00007FFF diff --git a/rust/operator-binary/src/controller/security.rs b/rust/operator-binary/src/controller/security.rs new file mode 100644 index 00000000..4ceb0747 --- /dev/null +++ b/rust/operator-binary/src/controller/security.rs @@ -0,0 +1,194 @@ +//! A helper module to process Apache Kafka security configuration +//! +//! This module merges the `tls` and `authentication` module and offers better accessibility +//! and helper functions +//! +//! This is required due to overlaps between TLS encryption and e.g. mTLS authentication or Kerberos +use snafu::{Snafu, ensure}; +use stackable_operator::{ + crd::authentication::core, + v2::types::{common::Port, kubernetes::SecretClassName}, +}; + +use crate::crd::{authentication::ResolvedAuthenticationClasses, tls, v1alpha1}; + +#[derive(Snafu, Debug)] +pub enum Error { + #[snafu(display("kerberos enablement requires TLS activation"))] + KerberosRequiresTls, +} + +/// Helper struct combining TLS settings for server and internal with the resolved AuthenticationClasses +pub struct ValidatedKafkaSecurity { + resolved_authentication_classes: ResolvedAuthenticationClasses, + internal_secret_class: Option, + server_secret_class: Option, + opa_secret_class: Option, +} + +impl ValidatedKafkaSecurity { + pub const BOOTSTRAP_PORT: Port = Port(9094); + // bootstrap: we will have a single named port with different values for + // secure (9095) and insecure (9094). The bootstrap listener is needed to + // be able to expose principals for both the broker and bootstrap in the + // JAAS configuration, so that clients can use both. + pub const BOOTSTRAP_PORT_NAME: &'static str = "bootstrap"; + pub const CLIENT_PORT: Port = Port(9092); + // ports + pub const CLIENT_PORT_NAME: &'static str = "kafka"; + // internal + pub const INTERNAL_PORT: Port = Port(19092); + pub const SECURE_BOOTSTRAP_PORT: Port = Port(9095); + pub const SECURE_CLIENT_PORT: Port = Port(9093); + pub const SECURE_CLIENT_PORT_NAME: &'static str = "kafka-tls"; + pub const SECURE_INTERNAL_PORT: Port = Port(19093); + + #[cfg(test)] + pub fn new( + resolved_authentication_classes: ResolvedAuthenticationClasses, + internal_secret_class: Option, + server_secret_class: Option, + opa_secret_class: Option, + ) -> Self { + Self { + resolved_authentication_classes, + internal_secret_class, + server_secret_class, + opa_secret_class, + } + } + + /// Build a [`ValidatedKafkaSecurity`] from already-resolved authentication classes. + /// + /// The async retrieval of [`ResolvedAuthenticationClasses`] now happens in the dereference + /// step of the controller; this constructor only reads TLS settings from the spec. + pub fn new_from_kafka_cluster( + kafka: &v1alpha1::KafkaCluster, + resolved_authentication_classes: ResolvedAuthenticationClasses, + opa_secret_class: Option, + ) -> Self { + ValidatedKafkaSecurity { + resolved_authentication_classes, + internal_secret_class: kafka + .spec + .cluster_config + .tls + .as_ref() + .map(|tls| tls.internal_secret_class.clone()) + .unwrap_or_else(tls::internal_tls_default), + server_secret_class: kafka + .spec + .cluster_config + .tls + .as_ref() + .and_then(|tls| tls.server_secret_class.clone()), + opa_secret_class, + } + } + + /// Check if TLS encryption is enabled. This could be due to: + /// - A provided server `SecretClass` + /// - A provided client `AuthenticationClass` + /// + /// This affects init container commands, Kafka configuration, volume mounts and + /// the Kafka client port + pub fn tls_enabled(&self) -> bool { + // TODO: This must be adapted if other authentication methods are supported and require TLS + self.tls_client_authentication_class().is_some() || self.tls_server_secret_class().is_some() + } + + /// Retrieve an optional TLS secret class for external client -> server communications. + pub fn tls_server_secret_class(&self) -> Option<&str> { + self.server_secret_class.as_ref().map(|s| s.as_ref()) + } + + /// Retrieve an optional TLS `AuthenticationClass`. + pub fn tls_client_authentication_class(&self) -> Option<&core::v1alpha1::AuthenticationClass> { + self.resolved_authentication_classes + .get_tls_authentication_class() + } + + /// Retrieve the optional internal `SecretClass`. + /// + /// Returns `None` when internal TLS is disabled (a plaintext cluster). + pub fn tls_internal_secret_class(&self) -> Option<&str> { + self.internal_secret_class.as_ref().map(|s| s.as_ref()) + } + + pub fn has_kerberos_enabled(&self) -> bool { + self.kerberos_secret_class().is_some() + } + + /// Retrieve the optional OPA TLS `SecretClass`. + pub fn opa_secret_class(&self) -> Option<&SecretClassName> { + self.opa_secret_class.as_ref() + } + + pub fn kerberos_secret_class(&self) -> Option { + if let Some(kerberos) = self + .resolved_authentication_classes + .get_kerberos_authentication_class() + { + match &kerberos.spec.provider { + core::v1alpha1::AuthenticationClassProvider::Kerberos(kerberos) => { + Some(kerberos.kerberos_secret_class.clone()) + } + _ => None, + } + } else { + None + } + } + + pub fn validate_authentication_methods(&self) -> Result<(), Error> { + // Client TLS authentication and Kerberos authentication are mutually + // exclusive, but this has already been checked when checking the + // authentication classes. When users enable Kerberos we require them + // to also enable TLS for a) maximum security and b) to limit the + // number of combinations we need to support. + if self.has_kerberos_enabled() { + ensure!(self.server_secret_class.is_some(), KerberosRequiresTlsSnafu); + } + + Ok(()) + } + + /// Return the Kafka (secure) client port depending on tls or authentication settings. + pub fn client_port(&self) -> Port { + if self.tls_enabled() { + Self::SECURE_CLIENT_PORT + } else { + Self::CLIENT_PORT + } + } + + pub fn bootstrap_port(&self) -> Port { + if self.tls_enabled() { + Self::SECURE_BOOTSTRAP_PORT + } else { + Self::BOOTSTRAP_PORT + } + } + + pub fn bootstrap_port_name(&self) -> &str { + Self::BOOTSTRAP_PORT_NAME + } + + /// Return the Kafka (secure) client port name depending on tls or authentication settings. + pub fn client_port_name(&self) -> &str { + if self.tls_enabled() { + Self::SECURE_CLIENT_PORT_NAME + } else { + Self::CLIENT_PORT_NAME + } + } + + /// Return the Kafka (secure) internal port depending on tls settings. + pub fn internal_port(&self) -> Port { + if self.tls_internal_secret_class().is_some() { + Self::SECURE_INTERNAL_PORT + } else { + Self::INTERNAL_PORT + } + } +} diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index a3e441a0..03cd8119 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -1,33 +1,54 @@ //! The validate step in the KafkaCluster controller. //! //! Synchronously validates inputs that don't require a Kubernetes client. Produces -//! [`ValidatedInputs`], consumed by the rest of `reconcile_kafka`. +//! [`ValidatedCluster`], consumed by the rest of `reconcile_kafka`. -use std::collections::HashMap; +use std::{collections::BTreeMap, str::FromStr}; -use product_config::{ProductConfigManager, types::PropertyNameKind}; -use snafu::{ResultExt, Snafu}; +use serde::Serialize; +use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ cli::OperatorEnvironmentOptions, - commons::product_image_selection::{self, ResolvedProductImage}, - product_config_utils::{ - ValidatedRoleConfigByPropertyKind, transform_all_roles_to_config, - validate_all_roles_and_groups_config, + commons::product_image_selection, + config::{fragment::FromFragment, merge::Merge}, + kube::ResourceExt, + product_logging::spec::Logging, + role_utils::{GenericRoleConfig, Role}, + schemars::JsonSchema, + v2::{ + builder::pod::container::{self, EnvVarName, EnvVarSet}, + controller_utils::{get_cluster_name, get_namespace, get_uid}, + product_logging::framework::{ + ValidatedContainerLogConfigChoice, VectorContainerLogConfig, + validate_logging_configuration_for_container, + }, + role_utils::{JavaCommonConfig, with_validated_config}, + types::kubernetes::ConfigMapName, }, }; use crate::{ - controller::dereference::DereferencedObjects, + controller::{ + RoleGroupName, ValidatedCluster, ValidatedClusterConfig, ValidatedKafkaConfig, + ValidatedRoleConfig, ValidatedRoleGroupConfig, + dereference::DereferencedObjects, + security::{self, ValidatedKafkaSecurity}, + }, crd::{ - self, CONTAINER_IMAGE_BASE_NAME, JVM_SECURITY_PROPERTIES_FILE, + self, CONTAINER_IMAGE_BASE_NAME, authentication::{self}, - authorization::KafkaAuthorizationConfig, - role::{KafkaRole, broker::BROKER_PROPERTIES_FILE, controller::CONTROLLER_PROPERTIES_FILE}, - security::{self, KafkaTlsSecurity}, + role::{ + AnyConfig, AnyConfigOverrides, KafkaRole, + broker::{BrokerConfig, BrokerContainer}, + controller::{ControllerConfig, ControllerContainer}, + }, v1alpha1, }, }; +/// The operator-managed env var carrying the Kafka cluster id. +const KAFKA_CLUSTER_ID_ENV: &str = "KAFKA_CLUSTER_ID"; + #[derive(Snafu, Debug)] pub enum Error { #[snafu(display("failed to resolve product image"))] @@ -44,34 +65,132 @@ pub enum Error { #[snafu(display("cluster object defines no '{role}' role"))] MissingKafkaRole { source: crd::Error, role: KafkaRole }, - #[snafu(display("failed to generate product config"))] - GenerateProductConfig { - source: stackable_operator::product_config_utils::Error, + #[snafu(display("failed to merge and validate the role group config"))] + ValidateRoleGroupConfig { + source: stackable_operator::config::fragment::ValidationError, + }, + + #[snafu(display("invalid environment variable name"))] + InvalidEnvVarName { source: container::Error }, + + #[snafu(display("invalid metadata manager"))] + InvalidMetadataManager { source: crate::crd::Error }, + + #[snafu(display("failed to resolve the cluster name"))] + ResolveClusterName { + source: stackable_operator::v2::controller_utils::Error, + }, + + #[snafu(display("failed to resolve the cluster namespace"))] + ResolveNamespace { + source: stackable_operator::v2::controller_utils::Error, + }, + + #[snafu(display("failed to resolve the cluster uid"))] + ResolveUid { + source: stackable_operator::v2::controller_utils::Error, }, - #[snafu(display("invalid product config"))] - InvalidProductConfig { - source: stackable_operator::product_config_utils::Error, + #[snafu(display("the role group name {role_group_name:?} is invalid"))] + ParseRoleGroupName { + source: stackable_operator::v2::macros::attributed_string_type::Error, + role_group_name: String, }, + + #[snafu(display("failed to validate the logging configuration"))] + ValidateLoggingConfig { + source: stackable_operator::v2::product_logging::framework::Error, + }, + + #[snafu(display( + "the Vector aggregator discovery ConfigMap name is required when the Vector agent is enabled" + ))] + MissingVectorAggregatorConfigMapName, } -type Result = std::result::Result; +/// Validated logging configuration for a Kafka role group's Kafka and (optional) Vector +/// containers. +/// +/// Produced up-front by [`validate_logging`] so that an invalid custom log `ConfigMap` name or a +/// missing Vector aggregator discovery `ConfigMap` name fails reconciliation during validation +/// rather than at resource-build time. +#[derive(Clone, Debug, PartialEq)] +pub struct ValidatedLogging { + pub kafka_container: ValidatedContainerLogConfigChoice, + pub vector_container: Option, +} + +/// Validates the logging configuration for a role group's Kafka and (optional) Vector container. +/// +/// `vector_aggregator_config_map_name` is the discovery `ConfigMap` name of the Vector +/// aggregator; it is required (and was validated into a [`ConfigMapName`]) only when the Vector +/// agent is enabled. Generic over the role's container enum so it serves both broker and +/// controller role groups. +fn validate_logging( + logging: &Logging, + kafka_container: C, + vector_container: C, + vector_aggregator_config_map_name: &Option, +) -> Result +where + C: Clone + std::fmt::Display + Ord, +{ + let kafka_container = validate_logging_configuration_for_container(logging, &kafka_container) + .context(ValidateLoggingConfigSnafu)?; + + let vector_container = if logging.enable_vector_agent { + let vector_aggregator_config_map_name = vector_aggregator_config_map_name + .clone() + .context(MissingVectorAggregatorConfigMapNameSnafu)?; + Some(VectorContainerLogConfig { + log_config: validate_logging_configuration_for_container(logging, &vector_container) + .context(ValidateLoggingConfigSnafu)?, + vector_aggregator_config_map_name, + }) + } else { + None + }; + + Ok(ValidatedLogging { + kafka_container, + vector_container, + }) +} + +/// Validates a broker role group's logging configuration. +fn validate_broker_logging( + config: &BrokerConfig, + vector_aggregator_config_map_name: &Option, +) -> Result { + validate_logging( + &config.logging, + BrokerContainer::Kafka, + BrokerContainer::Vector, + vector_aggregator_config_map_name, + ) +} -/// Synchronous inputs the rest of `reconcile_kafka` needs after dereferencing. -pub struct ValidatedInputs { - pub authorization_config: Option, - pub image: ResolvedProductImage, - pub kafka_security: KafkaTlsSecurity, - pub role_config: ValidatedRoleConfigByPropertyKind, +/// Validates a controller role group's logging configuration. +fn validate_controller_logging( + config: &ControllerConfig, + vector_aggregator_config_map_name: &Option, +) -> Result { + validate_logging( + &config.logging, + ControllerContainer::Kafka, + ControllerContainer::Vector, + vector_aggregator_config_map_name, + ) } +type Result = std::result::Result; + /// Validates the cluster spec and the dereferenced inputs. pub fn validate( kafka: &v1alpha1::KafkaCluster, dereferenced_objects: DereferencedObjects, operator_environment: &OperatorEnvironmentOptions, - product_config: &ProductConfigManager, -) -> Result { +) -> Result { let image = kafka .spec .image @@ -92,88 +211,231 @@ pub fn validate( .as_ref() .and_then(|cfg| cfg.secret_class.clone()); - let kafka_security = - KafkaTlsSecurity::new_from_kafka_cluster(kafka, authentication_classes, opa_secret_class); + let kafka_security = ValidatedKafkaSecurity::new_from_kafka_cluster( + kafka, + authentication_classes, + opa_secret_class, + ); kafka_security .validate_authentication_methods() .context(FailedToValidateAuthenticationMethodSnafu)?; - let role_config = validated_product_config(kafka, &image.product_version, product_config)?; + let cluster_id = kafka.cluster_id(); + + // The Vector aggregator discovery ConfigMap name. Validity is enforced by the `ConfigMapName` + // type on the CRD field. It is only required (per role group) when the Vector agent is + // enabled; see [`validate_logging`]. + let vector_aggregator_config_map_name = kafka + .spec + .cluster_config + .vector_aggregator_config_map_name + .clone(); - Ok(ValidatedInputs { - authorization_config: dereferenced_objects.authorization_config, + let mut role_configs: BTreeMap = BTreeMap::new(); + let mut role_group_configs: BTreeMap< + KafkaRole, + BTreeMap, + > = BTreeMap::new(); + + // Brokers always exist. + let broker_role = kafka.broker_role().context(MissingKafkaRoleSnafu { + role: KafkaRole::Broker, + })?; + let broker_groups = validate_role_group_configs( + broker_role, + BrokerConfig::default_config(&kafka.name_any(), &KafkaRole::Broker.to_string()), + cluster_id, + AnyConfig::Broker, + AnyConfigOverrides::Broker, + validate_broker_logging, + &vector_aggregator_config_map_name, + )?; + role_configs.insert( + KafkaRole::Broker, + ValidatedRoleConfig { + pdb: broker_role.role_config.pod_disruption_budget.clone(), + }, + ); + role_group_configs.insert(KafkaRole::Broker, broker_groups); + + // Controllers are optional: ZooKeeper-mode clusters have none, in which case they are simply + // absent from both maps and not reconciled. + if let Some(controller_role) = kafka.spec.controllers.as_ref() { + let controller_groups = validate_role_group_configs( + controller_role, + ControllerConfig::default_config(&kafka.name_any(), &KafkaRole::Controller.to_string()), + cluster_id, + AnyConfig::Controller, + AnyConfigOverrides::Controller, + validate_controller_logging, + &vector_aggregator_config_map_name, + )?; + role_configs.insert( + KafkaRole::Controller, + ValidatedRoleConfig { + pdb: controller_role.role_config.pod_disruption_budget.clone(), + }, + ); + role_group_configs.insert(KafkaRole::Controller, controller_groups); + } + + let metadata_manager = kafka + .effective_metadata_manager() + .context(InvalidMetadataManagerSnafu)?; + + let name = get_cluster_name(kafka).context(ResolveClusterNameSnafu)?; + let namespace = get_namespace(kafka).context(ResolveNamespaceSnafu)?; + let uid = get_uid(kafka).context(ResolveUidSnafu)?; + let cluster_domain = dereferenced_objects + .kubernetes_cluster_info + .cluster_domain + .clone(); + + Ok(ValidatedCluster::new( + name, + namespace, + uid, + cluster_domain, image, - kafka_security, - role_config, - }) + ValidatedClusterConfig { + kafka_security, + authorization_config: dereferenced_objects.authorization_config, + metadata_manager, + zookeeper_config_map_name: kafka.spec.cluster_config.zookeeper_config_map_name.clone(), + broker_id_pod_config_map_name: kafka + .spec + .cluster_config + .broker_id_pod_config_map_name + .clone(), + }, + role_configs, + role_group_configs, + )) } -fn validated_product_config( - kafka: &v1alpha1::KafkaCluster, - product_version: &str, - product_config: &ProductConfigManager, -) -> Result { - let mut role_config = HashMap::new(); - - let broker_role = [( - KafkaRole::Broker.to_string(), - ( - vec![ - PropertyNameKind::File(BROKER_PROPERTIES_FILE.to_string()), - PropertyNameKind::File(JVM_SECURITY_PROPERTIES_FILE.to_string()), - PropertyNameKind::Env, - ], - kafka - .broker_role() - .cloned() - .context(MissingKafkaRoleSnafu { - role: KafkaRole::Broker, - })? - .erase(), - ), - )] - .into(); - - let broker_role_config = - transform_all_roles_to_config(kafka, &broker_role).context(GenerateProductConfigSnafu)?; - - role_config.extend(broker_role_config); - - // We need this because controller_role() raises an error if non-existent, - // which would stop reconciliation. - if kafka.spec.controllers.is_some() { - let controller_role = [( - KafkaRole::Controller.to_string(), - ( - vec![ - PropertyNameKind::File(CONTROLLER_PROPERTIES_FILE.to_string()), - PropertyNameKind::File(JVM_SECURITY_PROPERTIES_FILE.to_string()), - PropertyNameKind::Env, - ], - kafka - .controller_role() - .cloned() - .context(MissingKafkaRoleSnafu { - role: KafkaRole::Controller, - })? - .erase(), - ), - )] - .into(); - - let controller_role_config = transform_all_roles_to_config(kafka, &controller_role) - .context(GenerateProductConfigSnafu)?; - - role_config.extend(controller_role_config); +/// Validates every role group of a role into a map keyed by role group name. +/// +/// Each role group is merged and validated via +/// [`with_validated_config`], which folds the config fragment (default <- role <- +/// role group) plus the `configOverrides`, `envOverrides`, `podOverrides` and +/// `jvmArgumentOverrides` (role group wins) into a single +/// [`ValidatedRoleGroupConfig`]. The concrete per-role validated config and overrides +/// are wrapped into the role-agnostic [`AnyConfig`]/[`AnyConfigOverrides`] via +/// `wrap_config`/`wrap_overrides`, and the operator-managed `KAFKA_CLUSTER_ID` is +/// injected into the env overrides. +fn validate_role_group_configs( + role: &Role, + default_config: Config, + cluster_id: Option<&str>, + wrap_config: fn(ValidatedConfig) -> AnyConfig, + wrap_overrides: fn(ConfigOverrides) -> AnyConfigOverrides, + validate_logging: fn(&ValidatedConfig, &Option) -> Result, + vector_aggregator_config_map_name: &Option, +) -> Result> +where + Config: Clone + Merge, + ValidatedConfig: FromFragment, + ConfigOverrides: Clone + Default + JsonSchema + Merge + Serialize, +{ + role.role_groups + .iter() + .map(|(role_group_name, role_group)| { + let merged = with_validated_config::< + ValidatedConfig, + JavaCommonConfig, + Config, + GenericRoleConfig, + ConfigOverrides, + >(role_group, role, &default_config) + .context(ValidateRoleGroupConfigSnafu)?; + + // The merge returns env overrides as a HashMap. Convert to an + // EnvVarSet (validating names early), then inject KAFKA_CLUSTER_ID. + let mut env_overrides = EnvVarSet::new(); + for (name, value) in merged.config.env_overrides { + let name = EnvVarName::from_str(&name).context(InvalidEnvVarNameSnafu)?; + env_overrides = env_overrides.with_value(&name, value); + } + let env_overrides = inject_cluster_id(env_overrides, cluster_id)?; + + let logging = + validate_logging(&merged.config.config, vector_aggregator_config_map_name)?; + + let validated = ValidatedRoleGroupConfig { + // Passed through as-is (including `None`) so an unset replica count lets a + // horizontal autoscaler own the StatefulSet's `.spec.replicas`. + replicas: merged.replicas, + config: ValidatedKafkaConfig { + config: wrap_config(merged.config.config), + logging, + }, + config_overrides: wrap_overrides(merged.config.config_overrides), + env_overrides, + // Kafka does not use CLI overrides; the field is carried (and merged upstream) + // but unused. + cli_overrides: merged.config.cli_overrides, + pod_overrides: merged.config.pod_overrides, + product_specific_common_config: merged.config.product_specific_common_config, + }; + let role_group_name = RoleGroupName::from_str(role_group_name).with_context(|_| { + ParseRoleGroupNameSnafu { + role_group_name: role_group_name.clone(), + } + })?; + Ok((role_group_name, validated)) + }) + .collect() +} + +/// Injects the operator-managed `KAFKA_CLUSTER_ID` into the merged env overrides, +/// but only when the user has not already set it via `envOverrides` (user value +/// wins). +fn inject_cluster_id(env_overrides: EnvVarSet, cluster_id: Option<&str>) -> Result { + let Some(cluster_id) = cluster_id else { + return Ok(env_overrides); + }; + let name = EnvVarName::from_str(KAFKA_CLUSTER_ID_ENV).context(InvalidEnvVarNameSnafu)?; + if env_overrides.get(&name).is_some() { + // The user set `KAFKA_CLUSTER_ID` via envOverrides; their value wins. + Ok(env_overrides) + } else { + Ok(env_overrides.with_value(&name, cluster_id)) } +} - validate_all_roles_and_groups_config( - product_version, - &role_config, - product_config, - false, - false, - ) - .context(InvalidProductConfigSnafu) +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use stackable_operator::v2::builder::pod::container::{EnvVarName, EnvVarSet}; + + use super::{KAFKA_CLUSTER_ID_ENV, inject_cluster_id}; + + fn cluster_id_value(env: &EnvVarSet) -> Option { + let name = EnvVarName::from_str(KAFKA_CLUSTER_ID_ENV).unwrap(); + env.get(&name).and_then(|var| var.value.clone()) + } + + #[test] + fn injects_cluster_id_when_absent() { + let env = inject_cluster_id(EnvVarSet::new(), Some("my-id")).unwrap(); + assert_eq!(cluster_id_value(&env), Some("my-id".to_string())); + } + + #[test] + fn user_cluster_id_override_wins() { + let name = EnvVarName::from_str(KAFKA_CLUSTER_ID_ENV).unwrap(); + let env = EnvVarSet::new().with_value(&name, "user-value"); + + let env = inject_cluster_id(env, Some("operator-value")).unwrap(); + + assert_eq!(cluster_id_value(&env), Some("user-value".to_string())); + } + + #[test] + fn without_cluster_id_nothing_is_injected() { + let env = inject_cluster_id(EnvVarSet::new(), None).unwrap(); + assert_eq!(cluster_id_value(&env), None); + } } diff --git a/rust/operator-binary/src/crd/affinity.rs b/rust/operator-binary/src/crd/affinity.rs index da01acca..152c8e58 100644 --- a/rust/operator-binary/src/crd/affinity.rs +++ b/rust/operator-binary/src/crd/affinity.rs @@ -32,7 +32,10 @@ mod tests { }, }; - use crate::crd::{KafkaRole, v1alpha1}; + use crate::{ + controller::test_support::{minimal_kafka, validated_cluster}, + crd::KafkaRole, + }; #[rstest] #[case(KafkaRole::Broker)] @@ -42,6 +45,8 @@ mod tests { kind: KafkaCluster metadata: name: simple-kafka + namespace: default + uid: 12345678-1234-1234-1234-123456789012 spec: image: productVersion: 3.9.2 @@ -53,9 +58,14 @@ mod tests { replicas: 1 "#; - let kafka: v1alpha1::KafkaCluster = - serde_yaml::from_str(input).expect("illegal test input"); - let merged_config = role.merged_config(&kafka, "default").unwrap(); + let kafka = minimal_kafka(input); + let validated = validated_cluster(&kafka); + let merged_config = validated + .role_group_configs + .get(&role) + .and_then(|groups| groups.get(&"default".parse().unwrap())) + .map(|rg| &rg.config.config) + .expect("role group should exist"); assert_eq!( merged_config.affinity, diff --git a/rust/operator-binary/src/crd/authorization.rs b/rust/operator-binary/src/crd/authorization.rs index e94ea533..829879a5 100644 --- a/rust/operator-binary/src/crd/authorization.rs +++ b/rust/operator-binary/src/crd/authorization.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use serde::{Deserialize, Serialize}; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ @@ -5,6 +7,7 @@ use stackable_operator::{ commons::opa::{OpaApiVersion, OpaConfig}, k8s_openapi::api::core::v1::ConfigMap, schemars::{self, JsonSchema}, + v2::types::kubernetes::SecretClassName, }; use crate::crd::v1alpha1; @@ -23,6 +26,11 @@ pub enum Error { source: stackable_operator::commons::opa::Error, }, + #[snafu(display("the OPA TLS secret class name (key `OPA_SECRET_CLASS`) is invalid"))] + ParseOpaSecretClass { + source: stackable_operator::v2::macros::attributed_string_type::Error, + }, + #[snafu(display("object defines no namespace"))] ObjectHasNoNamespace, } @@ -36,7 +44,7 @@ pub struct KafkaAuthorization { pub struct KafkaAuthorizationConfig { pub opa_connect: String, - pub secret_class: Option, + pub secret_class: Option, } impl KafkaAuthorization { @@ -60,7 +68,10 @@ impl KafkaAuthorization { namespace, })? .data - .and_then(|mut data| data.remove("OPA_SECRET_CLASS")); + .and_then(|mut data| data.remove("OPA_SECRET_CLASS")) + .map(|secret_class| SecretClassName::from_str(&secret_class)) + .transpose() + .context(ParseOpaSecretClassSnafu)?; let opa_connect = opa .full_document_url_from_config_map(client, kafka, Some("allow"), &OpaApiVersion::V1) .await diff --git a/rust/operator-binary/src/crd/listener.rs b/rust/operator-binary/src/crd/listener.rs index 7451fad3..7aabadad 100644 --- a/rust/operator-binary/src/crd/listener.rs +++ b/rust/operator-binary/src/crd/listener.rs @@ -3,21 +3,16 @@ use std::{ fmt::{Display, Formatter}, }; -use snafu::{OptionExt, Snafu}; -use stackable_operator::{ - kube::ResourceExt, role_utils::RoleGroupRef, utils::cluster_info::KubernetesClusterInfo, -}; -use strum::{EnumDiscriminants, EnumString}; - -use crate::crd::{STACKABLE_LISTENER_BROKER_DIR, security::KafkaTlsSecurity, v1alpha1}; +use strum::EnumString; -const LISTENER_LOCAL_ADDRESS: &str = "0.0.0.0"; +pub(crate) const LISTENER_LOCAL_ADDRESS: &str = "0.0.0.0"; -#[derive(Snafu, Debug, EnumDiscriminants)] -pub enum KafkaListenerError { - #[snafu(display("object has no namespace"))] - ObjectHasNoNamespace, -} +// Layout of the listener volume mounted by the listener operator: the default address is +// exposed under `/default-address/address` and per-port values under +// `/default-address/ports/`. +const LISTENER_DEFAULT_ADDRESS_DIR: &str = "default-address"; +const LISTENER_ADDRESS_FILE: &str = "address"; +const LISTENER_PORTS_DIR: &str = "ports"; #[derive(strum::Display, Debug, EnumString)] pub enum KafkaListenerProtocol { @@ -127,9 +122,9 @@ impl KafkaListenerName { #[derive(Debug)] pub struct KafkaListenerConfig { - listeners: Vec, - advertised_listeners: Vec, - listener_security_protocol_map: BTreeMap, + pub(crate) listeners: Vec, + pub(crate) advertised_listeners: Vec, + pub(crate) listener_security_protocol_map: BTreeMap, } impl KafkaListenerConfig { @@ -177,10 +172,10 @@ impl KafkaListenerConfig { } #[derive(Debug)] -struct KafkaListener { - name: KafkaListenerName, - host: String, - port: String, +pub(crate) struct KafkaListener { + pub(crate) name: KafkaListenerName, + pub(crate) host: String, + pub(crate) port: String, } impl Display for KafkaListener { @@ -189,493 +184,20 @@ impl Display for KafkaListener { } } -pub fn get_kafka_listener_config( - kafka: &v1alpha1::KafkaCluster, - kafka_security: &KafkaTlsSecurity, - rolegroup_ref: &RoleGroupRef, - cluster_info: &KubernetesClusterInfo, -) -> Result { - let pod_fqdn = pod_fqdn( - kafka, - &rolegroup_ref.rolegroup_headless_service_name(), - cluster_info, - )?; - let mut listeners = vec![]; - let mut advertised_listeners = vec![]; - let mut listener_security_protocol_map: BTreeMap = - BTreeMap::new(); - - // CLIENT - if kafka_security.has_kerberos_enabled() { - // 1) Kerberos and TLS authentication classes are mutually exclusive - listeners.push(KafkaListener { - name: KafkaListenerName::Client, - host: LISTENER_LOCAL_ADDRESS.to_string(), - port: KafkaTlsSecurity::SECURE_CLIENT_PORT.to_string(), - }); - advertised_listeners.push(KafkaListener { - name: KafkaListenerName::Client, - host: node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), - port: node_port_cmd( - STACKABLE_LISTENER_BROKER_DIR, - kafka_security.client_port_name(), - ), - }); - listener_security_protocol_map - .insert(KafkaListenerName::Client, KafkaListenerProtocol::SaslSsl); - } else if kafka_security.tls_client_authentication_class().is_some() - || kafka_security.tls_server_secret_class().is_some() - { - // 2) Client listener uses TLS (possibly with authentication) - listeners.push(KafkaListener { - name: KafkaListenerName::Client, - host: LISTENER_LOCAL_ADDRESS.to_string(), - port: kafka_security.client_port().to_string(), - }); - advertised_listeners.push(KafkaListener { - name: KafkaListenerName::Client, - host: node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), - port: node_port_cmd( - STACKABLE_LISTENER_BROKER_DIR, - kafka_security.client_port_name(), - ), - }); - listener_security_protocol_map - .insert(KafkaListenerName::Client, KafkaListenerProtocol::Ssl); - } else { - // 3) If no client auth or tls is required we expose CLIENT with PLAINTEXT - listeners.push(KafkaListener { - name: KafkaListenerName::Client, - host: LISTENER_LOCAL_ADDRESS.to_string(), - port: KafkaTlsSecurity::CLIENT_PORT.to_string(), - }); - advertised_listeners.push(KafkaListener { - name: KafkaListenerName::Client, - host: node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), - port: node_port_cmd( - STACKABLE_LISTENER_BROKER_DIR, - kafka_security.client_port_name(), - ), - }); - listener_security_protocol_map - .insert(KafkaListenerName::Client, KafkaListenerProtocol::Plaintext); - } - - // INTERNAL / CONTROLLER - if kafka_security.has_kerberos_enabled() || kafka_security.tls_internal_secret_class().is_some() - { - // 5) & 6) Kerberos and TLS authentication classes are mutually exclusive but both require internal tls to be used - listeners.push(KafkaListener { - name: KafkaListenerName::Internal, - host: LISTENER_LOCAL_ADDRESS.to_string(), - port: KafkaTlsSecurity::SECURE_INTERNAL_PORT.to_string(), - }); - advertised_listeners.push(KafkaListener { - name: KafkaListenerName::Internal, - host: pod_fqdn.to_string(), - port: KafkaTlsSecurity::SECURE_INTERNAL_PORT.to_string(), - }); - listener_security_protocol_map - .insert(KafkaListenerName::Internal, KafkaListenerProtocol::Ssl); - listener_security_protocol_map - .insert(KafkaListenerName::Controller, KafkaListenerProtocol::Ssl); - } else { - // 7) If no internal tls is required we expose INTERNAL as PLAINTEXT - listeners.push(KafkaListener { - name: KafkaListenerName::Internal, - host: LISTENER_LOCAL_ADDRESS.to_string(), - port: kafka_security.internal_port().to_string(), - }); - advertised_listeners.push(KafkaListener { - name: KafkaListenerName::Internal, - host: pod_fqdn.to_string(), - port: kafka_security.internal_port().to_string(), - }); - listener_security_protocol_map.insert( - KafkaListenerName::Internal, - KafkaListenerProtocol::Plaintext, - ); - listener_security_protocol_map.insert( - KafkaListenerName::Controller, - KafkaListenerProtocol::Plaintext, - ); - } - - // BOOTSTRAP - if kafka_security.has_kerberos_enabled() { - listeners.push(KafkaListener { - name: KafkaListenerName::Bootstrap, - host: LISTENER_LOCAL_ADDRESS.to_string(), - port: kafka_security.bootstrap_port().to_string(), - }); - advertised_listeners.push(KafkaListener { - name: KafkaListenerName::Bootstrap, - host: node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), - port: node_port_cmd( - STACKABLE_LISTENER_BROKER_DIR, - kafka_security.client_port_name(), - ), - }); - listener_security_protocol_map - .insert(KafkaListenerName::Bootstrap, KafkaListenerProtocol::SaslSsl); - } - - Ok(KafkaListenerConfig { - listeners, - advertised_listeners, - listener_security_protocol_map, - }) -} - pub fn node_address_cmd_env(directory: &str) -> String { - format!("$(cat {directory}/default-address/address)") + format!("$(cat {directory}/{LISTENER_DEFAULT_ADDRESS_DIR}/{LISTENER_ADDRESS_FILE})") } pub fn node_port_cmd_env(directory: &str, port_name: &str) -> String { - format!("$(cat {directory}/default-address/ports/{port_name})") + format!("$(cat {directory}/{LISTENER_DEFAULT_ADDRESS_DIR}/{LISTENER_PORTS_DIR}/{port_name})") } pub fn node_address_cmd(directory: &str) -> String { - format!("${{file:UTF-8:{directory}/default-address/address}}") + format!("${{file:UTF-8:{directory}/{LISTENER_DEFAULT_ADDRESS_DIR}/{LISTENER_ADDRESS_FILE}}}") } pub fn node_port_cmd(directory: &str, port_name: &str) -> String { - format!("${{file:UTF-8:{directory}/default-address/ports/{port_name}}}") -} - -pub fn pod_fqdn( - kafka: &v1alpha1::KafkaCluster, - sts_service_name: &str, - cluster_info: &KubernetesClusterInfo, -) -> Result { - Ok(format!( - "${{env:POD_NAME}}.{sts_service_name}.{namespace}.svc.{cluster_domain}", - namespace = kafka.namespace().context(ObjectHasNoNamespaceSnafu)?, - cluster_domain = cluster_info.cluster_domain - )) -} - -#[cfg(test)] -mod tests { - use stackable_operator::{ - builder::meta::ObjectMetaBuilder, - commons::networking::DomainName, - crd::authentication::{core, kerberos, tls}, - }; - - use super::*; - use crate::crd::{authentication::ResolvedAuthenticationClasses, role::KafkaRole}; - - fn default_cluster_info() -> KubernetesClusterInfo { - KubernetesClusterInfo { - cluster_domain: DomainName::try_from("cluster.local").unwrap(), - } - } - - #[test] - fn test_get_kafka_listeners_config() { - let kafka_cluster = r#" - apiVersion: kafka.stackable.tech/v1alpha1 - kind: KafkaCluster - metadata: - name: simple-kafka - namespace: default - spec: - image: - productVersion: 3.9.2 - clusterConfig: - authentication: - - authenticationClass: kafka-client-tls - tls: - internalSecretClass: internalTls - serverSecretClass: tls - zookeeperConfigMapName: xyz - "#; - let kafka: v1alpha1::KafkaCluster = - serde_yaml::from_str(kafka_cluster).expect("illegal test input"); - let kafka_security = KafkaTlsSecurity::new( - ResolvedAuthenticationClasses::new(vec![core::v1alpha1::AuthenticationClass { - metadata: ObjectMetaBuilder::new().name("auth-class").build(), - spec: core::v1alpha1::AuthenticationClassSpec { - provider: core::v1alpha1::AuthenticationClassProvider::Tls( - tls::v1alpha1::AuthenticationProvider { - client_cert_secret_class: Some("client-auth-secret-class".to_string()), - }, - ), - }, - }]), - "internalTls".to_string(), - Some("tls".to_string()), - None, - ); - let cluster_info = default_cluster_info(); - // "simple-kafka-broker-default" - let rolegroup_ref = kafka.rolegroup_ref(&KafkaRole::Broker, "default"); - let config = - get_kafka_listener_config(&kafka, &kafka_security, &rolegroup_ref, &cluster_info) - .unwrap(); - - assert_eq!( - config.listeners(), - format!( - "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", - name = KafkaListenerName::Client, - host = LISTENER_LOCAL_ADDRESS, - port = kafka_security.client_port(), - internal_name = KafkaListenerName::Internal, - internal_host = LISTENER_LOCAL_ADDRESS, - internal_port = kafka_security.internal_port(), - ) - ); - - assert_eq!( - config.advertised_listeners(), - format!( - "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", - name = KafkaListenerName::Client, - host = node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), - port = node_port_cmd( - STACKABLE_LISTENER_BROKER_DIR, - kafka_security.client_port_name() - ), - internal_name = KafkaListenerName::Internal, - internal_host = pod_fqdn( - &kafka, - &rolegroup_ref.rolegroup_headless_service_name(), - &cluster_info - ) - .unwrap(), - internal_port = kafka_security.internal_port(), - ) - ); - - assert_eq!( - config.listener_security_protocol_map(), - format!( - "{name}:{protocol},{internal_name}:{internal_protocol},{controller_name}:{controller_protocol}", - name = KafkaListenerName::Client, - protocol = KafkaListenerProtocol::Ssl, - internal_name = KafkaListenerName::Internal, - internal_protocol = KafkaListenerProtocol::Ssl, - controller_name = KafkaListenerName::Controller, - controller_protocol = KafkaListenerProtocol::Ssl, - ) - ); - - let kafka_security = KafkaTlsSecurity::new( - ResolvedAuthenticationClasses::new(vec![]), - "tls".to_string(), - Some("tls".to_string()), - None, - ); - let config = - get_kafka_listener_config(&kafka, &kafka_security, &rolegroup_ref, &cluster_info) - .unwrap(); - - assert_eq!( - config.listeners(), - format!( - "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", - name = KafkaListenerName::Client, - host = LISTENER_LOCAL_ADDRESS, - port = kafka_security.client_port(), - internal_name = KafkaListenerName::Internal, - internal_host = LISTENER_LOCAL_ADDRESS, - internal_port = kafka_security.internal_port(), - ) - ); - - assert_eq!( - config.advertised_listeners(), - format!( - "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", - name = KafkaListenerName::Client, - host = node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), - port = node_port_cmd( - STACKABLE_LISTENER_BROKER_DIR, - kafka_security.client_port_name() - ), - internal_name = KafkaListenerName::Internal, - internal_host = pod_fqdn( - &kafka, - &rolegroup_ref.rolegroup_headless_service_name(), - &cluster_info - ) - .unwrap(), - internal_port = kafka_security.internal_port(), - ) - ); - - assert_eq!( - config.listener_security_protocol_map(), - format!( - "{name}:{protocol},{internal_name}:{internal_protocol},{controller_name}:{controller_protocol}", - name = KafkaListenerName::Client, - protocol = KafkaListenerProtocol::Ssl, - internal_name = KafkaListenerName::Internal, - internal_protocol = KafkaListenerProtocol::Ssl, - controller_name = KafkaListenerName::Controller, - controller_protocol = KafkaListenerProtocol::Ssl, - ) - ); - - let kafka_security = KafkaTlsSecurity::new( - ResolvedAuthenticationClasses::new(vec![]), - "".to_string(), - None, - None, - ); - - let config = - get_kafka_listener_config(&kafka, &kafka_security, &rolegroup_ref, &cluster_info) - .unwrap(); - - assert_eq!( - config.listeners(), - format!( - "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", - name = KafkaListenerName::Client, - host = LISTENER_LOCAL_ADDRESS, - port = kafka_security.client_port(), - internal_name = KafkaListenerName::Internal, - internal_host = LISTENER_LOCAL_ADDRESS, - internal_port = kafka_security.internal_port(), - ) - ); - - assert_eq!( - config.advertised_listeners(), - format!( - "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", - name = KafkaListenerName::Client, - host = node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), - port = node_port_cmd( - STACKABLE_LISTENER_BROKER_DIR, - kafka_security.client_port_name() - ), - internal_name = KafkaListenerName::Internal, - internal_host = pod_fqdn( - &kafka, - &rolegroup_ref.rolegroup_headless_service_name(), - &cluster_info - ) - .unwrap(), - internal_port = kafka_security.internal_port(), - ) - ); - - assert_eq!( - config.listener_security_protocol_map(), - format!( - "{name}:{protocol},{internal_name}:{internal_protocol},{controller_name}:{controller_protocol}", - name = KafkaListenerName::Client, - protocol = KafkaListenerProtocol::Plaintext, - internal_name = KafkaListenerName::Internal, - internal_protocol = KafkaListenerProtocol::Plaintext, - controller_name = KafkaListenerName::Controller, - controller_protocol = KafkaListenerProtocol::Plaintext, - ) - ); - } - - #[test] - fn test_get_kafka_kerberos_listeners_config() { - let kafka_cluster = r#" - apiVersion: kafka.stackable.tech/v1alpha1 - kind: KafkaCluster - metadata: - name: simple-kafka - namespace: default - spec: - image: - productVersion: 3.9.2 - clusterConfig: - authentication: - - authenticationClass: kafka-kerberos - tls: - serverSecretClass: tls - zookeeperConfigMapName: xyz - "#; - let kafka: v1alpha1::KafkaCluster = - serde_yaml::from_str(kafka_cluster).expect("illegal test input"); - let kafka_security = KafkaTlsSecurity::new( - ResolvedAuthenticationClasses::new(vec![core::v1alpha1::AuthenticationClass { - metadata: ObjectMetaBuilder::new().name("auth-class").build(), - spec: core::v1alpha1::AuthenticationClassSpec { - provider: core::v1alpha1::AuthenticationClassProvider::Kerberos( - kerberos::v1alpha1::AuthenticationProvider { - kerberos_secret_class: "kerberos-secret-class".to_string(), - }, - ), - }, - }]), - "tls".to_string(), - Some("tls".to_string()), - None, - ); - let cluster_info = default_cluster_info(); - // "simple-kafka-broker-default" - let rolegroup_ref = kafka.rolegroup_ref(&KafkaRole::Broker, "default"); - let config = - get_kafka_listener_config(&kafka, &kafka_security, &rolegroup_ref, &cluster_info) - .unwrap(); - - assert_eq!( - config.listeners(), - format!( - "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port},{bootstrap_name}://{bootstrap_host}:{bootstrap_port}", - name = KafkaListenerName::Client, - host = LISTENER_LOCAL_ADDRESS, - port = kafka_security.client_port(), - internal_name = KafkaListenerName::Internal, - internal_host = LISTENER_LOCAL_ADDRESS, - internal_port = kafka_security.internal_port(), - bootstrap_name = KafkaListenerName::Bootstrap, - bootstrap_host = LISTENER_LOCAL_ADDRESS, - bootstrap_port = kafka_security.bootstrap_port(), - ) - ); - - assert_eq!( - config.advertised_listeners(), - format!( - "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port},{bootstrap_name}://{bootstrap_host}:{bootstrap_port}", - name = KafkaListenerName::Client, - host = node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), - port = node_port_cmd( - STACKABLE_LISTENER_BROKER_DIR, - kafka_security.client_port_name() - ), - internal_name = KafkaListenerName::Internal, - internal_host = pod_fqdn( - &kafka, - &rolegroup_ref.rolegroup_headless_service_name(), - &cluster_info - ) - .unwrap(), - internal_port = kafka_security.internal_port(), - bootstrap_name = KafkaListenerName::Bootstrap, - bootstrap_host = node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), - bootstrap_port = node_port_cmd( - STACKABLE_LISTENER_BROKER_DIR, - kafka_security.client_port_name() - ), - ) - ); - - assert_eq!( - config.listener_security_protocol_map(), - format!( - "{name}:{protocol},{internal_name}:{internal_protocol},{bootstrap_name}:{bootstrap_protocol},{controller_name}:{controller_protocol}", - name = KafkaListenerName::Client, - protocol = KafkaListenerProtocol::SaslSsl, - internal_name = KafkaListenerName::Internal, - internal_protocol = KafkaListenerProtocol::Ssl, - bootstrap_name = KafkaListenerName::Bootstrap, - bootstrap_protocol = KafkaListenerProtocol::SaslSsl, - controller_name = KafkaListenerName::Controller, - controller_protocol = KafkaListenerProtocol::Ssl, - ) - ); - } + format!( + "${{file:UTF-8:{directory}/{LISTENER_DEFAULT_ADDRESS_DIR}/{LISTENER_PORTS_DIR}/{port_name}}}" + ) } diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index d662de30..26e7960e 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -3,11 +3,8 @@ pub mod authentication; pub mod authorization; pub mod listener; pub mod role; -pub mod security; pub mod tls; -use std::collections::{BTreeMap, HashMap}; - use authentication::KafkaAuthentication; use serde::{Deserialize, Serialize}; use snafu::{OptionExt, Snafu}; @@ -16,24 +13,28 @@ use stackable_operator::{ cluster_operation::ClusterOperation, networking::DomainName, product_image_selection::ProductImage, }, - config_overrides::{KeyValueConfigOverrides, KeyValueOverridesProvider}, + config::merge::Merge, deep_merger::ObjectOverrides, kube::{CustomResource, runtime::reflector::ObjectRef}, - role_utils::{GenericRoleConfig, JavaCommonConfig, Role, RoleGroupRef}, + role_utils::{GenericRoleConfig, Role}, schemars::{self, JsonSchema}, status::condition::{ClusterCondition, HasStatusCondition}, - utils::cluster_info::KubernetesClusterInfo, + v2::{ + config_overrides::KeyValueConfigOverrides, + role_utils::JavaCommonConfig, + types::{ + common::Port, + kubernetes::{ConfigMapName, NamespaceName, ServiceName, StatefulSetName}, + }, + }, versioned::versioned, }; use strum::{Display, EnumIter, EnumString}; -use crate::{ - config::node_id_hasher::node_id_hash32_offset, - crd::{ - authorization::KafkaAuthorization, - role::{KafkaRole, broker::BrokerConfigFragment, controller::ControllerConfigFragment}, - tls::KafkaTls, - }, +use crate::crd::{ + authorization::KafkaAuthorization, + role::{KafkaRole, broker::BrokerConfigFragment, controller::ControllerConfigFragment}, + tls::KafkaTls, }; pub const CONTAINER_IMAGE_BASE_NAME: &str = "kafka"; @@ -42,9 +43,7 @@ pub const OPERATOR_NAME: &str = "kafka.stackable.tech"; pub const FIELD_MANAGER: &str = "kafka-operator"; // metrics pub const METRICS_PORT_NAME: &str = "metrics"; -pub const METRICS_PORT: u16 = 9606; -// config files -pub const JVM_SECURITY_PROPERTIES_FILE: &str = "security.properties"; +pub const METRICS_PORT: Port = Port(9606); // env vars pub const KAFKA_HEAP_OPTS: &str = "KAFKA_HEAP_OPTS"; // server_properties @@ -56,9 +55,16 @@ pub const STACKABLE_LISTENER_BROKER_DIR: &str = "/stackable/listener-broker"; pub const STACKABLE_LISTENER_BOOTSTRAP_DIR: &str = "/stackable/listener-bootstrap"; pub const STACKABLE_DATA_DIR: &str = "/stackable/data"; pub const STACKABLE_CONFIG_DIR: &str = "/stackable/config"; +pub const STACKABLE_CONFIG_DIR_NAME: &str = "config"; // kerberos pub const STACKABLE_KERBEROS_DIR: &str = "/stackable/kerberos"; pub const STACKABLE_KERBEROS_KRB5_PATH: &str = "/stackable/kerberos/krb5.conf"; +// logging +pub const STACKABLE_LOG_CONFIG_DIR: &str = "/stackable/log_config"; +pub const STACKABLE_LOG_CONFIG_DIR_NAME: &str = "log-config"; +pub const STACKABLE_LOG_DIR_NAME: &str = "log"; +pub const BROKER_ID_POD_MAP_DIR: &str = "/stackable/broker-id-pod-map"; +pub const BROKER_ID_POD_MAP_DIR_NAME: &str = "broker-id-pod-map-dir"; #[derive(Snafu, Debug)] pub enum Error { @@ -70,9 +76,6 @@ pub enum Error { #[snafu(display("The Kafka role [{role}] is missing from spec"))] MissingRole { role: String }, - #[snafu(display("Object has no namespace associated"))] - NoNamespace, - #[snafu(display( "Kafka version 4 and higher requires a Kraft controller (configured via `spec.controller`)" ))] @@ -82,16 +85,6 @@ pub enum Error { "Kraft controller (`spec.controller`) and ZooKeeper (`spec.clusterConfig.zookeeperConfigMapName`) are configured. Please only choose one" ))] KraftAndZookeeperConfigured, - - #[snafu(display( - "Could not calculate 'node.id' hash offset for role '{role}' and rolegroup '{rolegroup}' which collides with role '{coliding_role}' and rolegroup '{colliding_rolegroup}'. Please try to rename one of the rolegroups." - ))] - KafkaNodeIdHashCollision { - role: KafkaRole, - rolegroup: String, - coliding_role: KafkaRole, - colliding_rolegroup: String, - }, } pub type BrokerRole = Role< @@ -179,14 +172,14 @@ pub mod versioned { /// Follow the [logging tutorial](DOCS_BASE_URL_PLACEHOLDER/tutorials/logging-vector-aggregator) /// to learn how to configure log aggregation with Vector. #[serde(skip_serializing_if = "Option::is_none")] - pub vector_aggregator_config_map_name: Option, + pub vector_aggregator_config_map_name: Option, /// Provide the name of the ZooKeeper [discovery ConfigMap](DOCS_BASE_URL_PLACEHOLDER/concepts/service_discovery) /// here. When using the [Stackable operator for Apache ZooKeeper](DOCS_BASE_URL_PLACEHOLDER/zookeeper/) /// to deploy a ZooKeeper cluster, this will simply be the name of your ZookeeperCluster resource. /// This can only be used up to Kafka version 3.9.x. Since Kafka 4.0.0, ZooKeeper support was dropped. /// Please use the 'controller' role instead. - pub zookeeper_config_map_name: Option, + pub zookeeper_config_map_name: Option, /// Metadata manager to use for the Kafka cluster. /// @@ -237,43 +230,27 @@ pub mod versioned { /// because previously broker ids were generated by Kafka and not the operator. /// #[serde(skip_serializing_if = "Option::is_none")] - pub broker_id_pod_config_map_name: Option, + pub broker_id_pod_config_map_name: Option, } - #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] + #[derive(Clone, Debug, Default, Deserialize, JsonSchema, Merge, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct KafkaBrokerConfigOverrides { - #[serde( - default, - rename = "broker.properties", - skip_serializing_if = "Option::is_none" - )] - pub broker_properties: Option, + #[serde(default, rename = "broker.properties")] + pub broker_properties: KeyValueConfigOverrides, - #[serde( - default, - rename = "security.properties", - skip_serializing_if = "Option::is_none" - )] - pub security_properties: Option, + #[serde(default, rename = "security.properties")] + pub security_properties: KeyValueConfigOverrides, } - #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] + #[derive(Clone, Debug, Default, Deserialize, JsonSchema, Merge, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct KafkaControllerConfigOverrides { - #[serde( - default, - rename = "controller.properties", - skip_serializing_if = "Option::is_none" - )] - pub controller_properties: Option, + #[serde(default, rename = "controller.properties")] + pub controller_properties: KeyValueConfigOverrides, - #[serde( - default, - rename = "security.properties", - skip_serializing_if = "Option::is_none" - )] - pub security_properties: Option, + #[serde(default, rename = "security.properties")] + pub security_properties: KeyValueConfigOverrides, } } @@ -291,32 +268,6 @@ impl Default for v1alpha1::KafkaClusterConfig { } } -impl KeyValueOverridesProvider for v1alpha1::KafkaBrokerConfigOverrides { - fn get_key_value_overrides(&self, file: &str) -> BTreeMap> { - let field = match file { - role::broker::BROKER_PROPERTIES_FILE => self.broker_properties.as_ref(), - JVM_SECURITY_PROPERTIES_FILE => self.security_properties.as_ref(), - _ => None, - }; - field - .map(KeyValueConfigOverrides::as_product_config_overrides) - .unwrap_or_default() - } -} - -impl KeyValueOverridesProvider for v1alpha1::KafkaControllerConfigOverrides { - fn get_key_value_overrides(&self, file: &str) -> BTreeMap> { - let field = match file { - role::controller::CONTROLLER_PROPERTIES_FILE => self.controller_properties.as_ref(), - JVM_SECURITY_PROPERTIES_FILE => self.security_properties.as_ref(), - _ => None, - }; - field - .map(KeyValueConfigOverrides::as_product_config_overrides) - .unwrap_or_default() - } -} - impl HasStatusCondition for v1alpha1::KafkaCluster { fn conditions(&self) -> Vec { match &self.status { @@ -329,7 +280,7 @@ impl HasStatusCondition for v1alpha1::KafkaCluster { impl v1alpha1::KafkaCluster { pub fn effective_metadata_manager(&self) -> Result { match &self.spec.cluster_config.metadata_manager { - Some(manager) => match manager.clone() { + Some(manager) => match manager { MetadataManager::ZooKeeper => { if !self.spec.image.product_version().starts_with("3.") { Err(Error::Kafka4RequiresKraftMetadataManager) @@ -369,143 +320,26 @@ impl v1alpha1::KafkaCluster { }) } - /// The name of the load-balanced Kubernetes Service providing the bootstrap address. Kafka clients will use this - /// to get a list of broker addresses and will use those to transmit data to the correct broker. - pub fn bootstrap_service_name(&self, rolegroup: &RoleGroupRef) -> String { - format!("{}-bootstrap", rolegroup.object_name()) - } - - /// Metadata about a rolegroup - pub fn rolegroup_ref( - &self, - role: &KafkaRole, - group_name: impl Into, - ) -> RoleGroupRef { - RoleGroupRef { - cluster: ObjectRef::from_obj(self), - role: role.to_string(), - role_group: group_name.into(), - } - } - - pub fn role_config(&self, role: &KafkaRole) -> Option<&GenericRoleConfig> { - match role { - KafkaRole::Broker => self.spec.brokers.as_ref().map(|b| &b.role_config), - KafkaRole::Controller => self.spec.controllers.as_ref().map(|b| &b.role_config), - } - } - pub fn broker_role(&self) -> Result<&BrokerRole, Error> { self.spec.brokers.as_ref().context(MissingRoleSnafu { role: KafkaRole::Broker.to_string(), }) } - - pub fn controller_role(&self) -> Result<&ControllerRole, Error> { - self.spec.controllers.as_ref().context(MissingRoleSnafu { - role: KafkaRole::Controller.to_string(), - }) - } - - /// List pod descriptors for a given role and all its rolegroups. - /// If no role is provided, pod descriptors for all roles (and all groups) are listed. - /// We try to predict the pods here rather than looking at the current cluster state in order to - /// avoid instance churn. - pub fn pod_descriptors( - &self, - requested_kafka_role: Option<&KafkaRole>, - cluster_info: &KubernetesClusterInfo, - client_port: u16, - ) -> Result, Error> { - let namespace = self.metadata.namespace.clone().context(NoNamespaceSnafu)?; - let mut pod_descriptors = Vec::new(); - let mut seen_hashes = HashMap::::new(); - - for current_role in KafkaRole::roles() { - let rolegroup_replicas = self.extract_rolegroup_replicas(¤t_role)?; - for (rolegroup, replicas) in rolegroup_replicas { - let rolegroup_ref = self.rolegroup_ref(¤t_role, &rolegroup); - let node_id_hash_offset = node_id_hash32_offset(&rolegroup_ref); - - // check collisions - match seen_hashes.get(&node_id_hash_offset) { - Some((coliding_role, coliding_rolegroup)) => { - return KafkaNodeIdHashCollisionSnafu { - role: current_role.clone(), - rolegroup: rolegroup.clone(), - coliding_role: coliding_role.clone(), - colliding_rolegroup: coliding_rolegroup.to_string(), - } - .fail(); - } - None => { - seen_hashes.insert(node_id_hash_offset, (current_role.clone(), rolegroup)) - } - }; - - // If no specific role is requested, or the current role matches the requested one, add pod descriptors - if requested_kafka_role.is_none() || Some(¤t_role) == requested_kafka_role { - for replica in 0..replicas { - pod_descriptors.push(KafkaPodDescriptor { - namespace: namespace.clone(), - role: current_role.to_string(), - role_group_service_name: rolegroup_ref - .rolegroup_headless_service_name(), - role_group_statefulset_name: rolegroup_ref.object_name(), - replica, - cluster_domain: cluster_info.cluster_domain.clone(), - node_id: node_id_hash_offset + u32::from(replica), - client_port, - }); - } - } - } - } - - Ok(pod_descriptors) - } - - fn extract_rolegroup_replicas( - &self, - kafka_role: &KafkaRole, - ) -> Result, Error> { - Ok(match kafka_role { - KafkaRole::Broker => self - .broker_role() - .iter() - .flat_map(|role| &role.role_groups) - .flat_map(|(rolegroup_name, rolegroup)| { - std::iter::once((rolegroup_name.to_string(), rolegroup.replicas.unwrap_or(0))) - }) - // Order rolegroups consistently, to avoid spurious downstream rewrites - .collect::>(), - - KafkaRole::Controller => self - .controller_role() - .iter() - .flat_map(|role| &role.role_groups) - .flat_map(|(rolegroup_name, rolegroup)| { - std::iter::once((rolegroup_name.to_string(), rolegroup.replicas.unwrap_or(0))) - }) - // Order rolegroups consistently, to avoid spurious downstream rewrites - .collect::>(), - }) - } } /// Reference to a single `Pod` that is a component of a [`KafkaCluster`] /// /// Used for service discovery. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, PartialEq, Eq)] pub struct KafkaPodDescriptor { - namespace: String, - role_group_statefulset_name: String, - role_group_service_name: String, - replica: u16, - cluster_domain: DomainName, - node_id: u32, - pub role: String, - pub client_port: u16, + pub(crate) namespace: NamespaceName, + pub(crate) role_group_statefulset_name: StatefulSetName, + pub(crate) role_group_service_name: ServiceName, + pub(crate) replica: u16, + pub(crate) cluster_domain: DomainName, + pub(crate) node_id: u32, + pub role: KafkaRole, + pub client_port: Port, } impl KafkaPodDescriptor { @@ -542,15 +376,6 @@ impl KafkaPodDescriptor { fqdn = self.fqdn(), ) } - - pub fn as_quorum_voter(&self) -> String { - format!( - "{node_id}@{fqdn}:{port}", - node_id = self.node_id, - port = self.client_port, - fqdn = self.fqdn(), - ) - } } #[derive(Clone, Default, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] @@ -582,11 +407,13 @@ pub enum MetadataManager { #[cfg(test)] mod tests { use rstest::rstest; - use stackable_operator::versioned::test_utils::RoundtripTestData; + use stackable_operator::{ + v2::types::kubernetes::SecretClassName, versioned::test_utils::RoundtripTestData, + }; use super::*; - fn get_server_secret_class(kafka: &v1alpha1::KafkaCluster) -> Option { + fn get_server_secret_class(kafka: &v1alpha1::KafkaCluster) -> Option { kafka .spec .cluster_config @@ -595,7 +422,7 @@ mod tests { .and_then(|tls| tls.server_secret_class.clone()) } - fn get_internal_secret_class(kafka: &v1alpha1::KafkaCluster) -> String { + fn get_internal_secret_class(kafka: &v1alpha1::KafkaCluster) -> Option { kafka .spec .cluster_config @@ -644,8 +471,8 @@ mod tests { let kafka: v1alpha1::KafkaCluster = serde_yaml::from_str(input).expect("illegal test input"); assert_eq!( - get_server_secret_class(&kafka).unwrap(), - "simple-kafka-server-tls".to_string() + get_server_secret_class(&kafka).unwrap().to_string(), + "simple-kafka-server-tls" ); assert_eq!( get_internal_secret_class(&kafka), @@ -691,8 +518,8 @@ mod tests { serde_yaml::from_str(input).expect("illegal test input"); assert_eq!(get_server_secret_class(&kafka), tls::server_tls_default()); assert_eq!( - get_internal_secret_class(&kafka), - "simple-kafka-internal-tls".to_string() + get_internal_secret_class(&kafka).map(|s| s.to_string()), + Some("simple-kafka-internal-tls".to_string()) ); } @@ -734,8 +561,8 @@ mod tests { serde_yaml::from_str(input).expect("illegal test input"); assert_eq!(get_server_secret_class(&kafka), tls::server_tls_default()); assert_eq!( - get_internal_secret_class(&kafka), - "simple-kafka-internal-tls".to_string() + get_internal_secret_class(&kafka).map(|s| s.to_string()), + Some("simple-kafka-internal-tls".to_string()) ); let input = r#" @@ -754,7 +581,7 @@ mod tests { let kafka: v1alpha1::KafkaCluster = serde_yaml::from_str(input).expect("illegal test input"); assert_eq!( - get_server_secret_class(&kafka), + get_server_secret_class(&kafka).map(|s| s.to_string()), Some("simple-kafka-server-tls".to_string()) ); assert_eq!( diff --git a/rust/operator-binary/src/crd/role/broker.rs b/rust/operator-binary/src/crd/role/broker.rs index 70ac85d0..c1eafa34 100644 --- a/rust/operator-binary/src/crd/role/broker.rs +++ b/rust/operator-binary/src/crd/role/broker.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeMap; - use serde::{Deserialize, Serialize}; use stackable_operator::{ commons::resources::{ @@ -8,18 +6,13 @@ use stackable_operator::{ }, config::{fragment::Fragment, merge::Merge}, k8s_openapi::apimachinery::pkg::api::resource::Quantity, - product_config_utils::Configuration, product_logging::{self, spec::Logging}, schemars::{self, JsonSchema}, + v2::types::kubernetes::ListenerClassName, }; use strum::{Display, EnumIter}; -use crate::crd::{ - role::commons::{CommonConfig, Storage, StorageFragment}, - v1alpha1, -}; - -pub const BROKER_PROPERTIES_FILE: &str = "broker.properties"; +use crate::crd::role::commons::{CommonConfig, Storage, StorageFragment}; #[derive( Clone, @@ -39,11 +32,10 @@ pub const BROKER_PROPERTIES_FILE: &str = "broker.properties"; pub enum BrokerContainer { Vector, KcatProber, - GetService, Kafka, } -#[derive(Debug, Default, PartialEq, Fragment, JsonSchema)] +#[derive(Clone, Debug, PartialEq, Fragment, JsonSchema)] #[fragment_attrs( derive( Clone, @@ -62,10 +54,10 @@ pub struct BrokerConfig { pub common_config: CommonConfig, /// The ListenerClass used for bootstrapping new clients. Should use a stable ListenerClass to avoid unnecessary client restarts (such as `cluster-internal` or `external-stable`). - pub bootstrap_listener_class: String, + pub bootstrap_listener_class: ListenerClassName, /// The ListenerClass used for connecting to brokers. Should use a direct connection ListenerClass to minimize cost and minimize performance overhead (such as `cluster-internal` or `external-unstable`). - pub broker_listener_class: String, + pub broker_listener_class: ListenerClassName, #[fragment_attrs(serde(default))] pub logging: Logging, @@ -78,8 +70,16 @@ impl BrokerConfig { pub fn default_config(cluster_name: &str, role: &str) -> BrokerConfigFragment { BrokerConfigFragment { common_config: CommonConfig::default_config(cluster_name, role), - bootstrap_listener_class: Some("cluster-internal".to_string()), - broker_listener_class: Some("cluster-internal".to_string()), + bootstrap_listener_class: Some( + "cluster-internal" + .parse() + .expect("\"cluster-internal\" is a valid listener class name"), + ), + broker_listener_class: Some( + "cluster-internal" + .parse() + .expect("\"cluster-internal\" is a valid listener class name"), + ), logging: product_logging::spec::default_logging(), resources: ResourcesFragment { cpu: CpuLimitsFragment { @@ -101,39 +101,3 @@ impl BrokerConfig { } } } - -impl Configuration for BrokerConfigFragment { - type Configurable = v1alpha1::KafkaCluster; - - fn compute_env( - &self, - resource: &Self::Configurable, - _role_name: &str, - ) -> Result>, stackable_operator::product_config_utils::Error> - { - let mut result = BTreeMap::new(); - if let Some(cluster_id) = resource.cluster_id() { - result.insert("KAFKA_CLUSTER_ID".to_string(), Some(cluster_id.to_string())); - } - Ok(result) - } - - fn compute_cli( - &self, - _resource: &Self::Configurable, - _role_name: &str, - ) -> Result>, stackable_operator::product_config_utils::Error> - { - Ok(BTreeMap::new()) - } - - fn compute_files( - &self, - _resource: &Self::Configurable, - _role_name: &str, - _file: &str, - ) -> Result>, stackable_operator::product_config_utils::Error> - { - Ok(BTreeMap::new()) - } -} diff --git a/rust/operator-binary/src/crd/role/controller.rs b/rust/operator-binary/src/crd/role/controller.rs index bf1468b6..ec025eab 100644 --- a/rust/operator-binary/src/crd/role/controller.rs +++ b/rust/operator-binary/src/crd/role/controller.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeMap; - use serde::{Deserialize, Serialize}; use stackable_operator::{ commons::resources::{ @@ -8,18 +6,12 @@ use stackable_operator::{ }, config::{fragment::Fragment, merge::Merge}, k8s_openapi::apimachinery::pkg::api::resource::Quantity, - product_config_utils::Configuration, product_logging::{self, spec::Logging}, schemars::{self, JsonSchema}, }; use strum::{Display, EnumIter}; -use crate::crd::{ - role::commons::{CommonConfig, Storage, StorageFragment}, - v1alpha1, -}; - -pub const CONTROLLER_PROPERTIES_FILE: &str = "controller.properties"; +use crate::crd::role::commons::{CommonConfig, Storage, StorageFragment}; #[derive( Clone, @@ -41,7 +33,7 @@ pub enum ControllerContainer { Kafka, } -#[derive(Debug, Default, PartialEq, Fragment, JsonSchema)] +#[derive(Clone, Debug, Default, PartialEq, Fragment, JsonSchema)] #[fragment_attrs( derive( Clone, @@ -91,39 +83,3 @@ impl ControllerConfig { } } } - -impl Configuration for ControllerConfigFragment { - type Configurable = v1alpha1::KafkaCluster; - - fn compute_env( - &self, - resource: &Self::Configurable, - _role_name: &str, - ) -> Result>, stackable_operator::product_config_utils::Error> - { - let mut result = BTreeMap::new(); - if let Some(cluster_id) = resource.cluster_id() { - result.insert("KAFKA_CLUSTER_ID".to_string(), Some(cluster_id.to_string())); - } - Ok(result) - } - - fn compute_cli( - &self, - _resource: &Self::Configurable, - _role_name: &str, - ) -> Result>, stackable_operator::product_config_utils::Error> - { - Ok(BTreeMap::new()) - } - - fn compute_files( - &self, - _resource: &Self::Configurable, - _role_name: &str, - _file: &str, - ) -> Result>, stackable_operator::product_config_utils::Error> - { - Ok(BTreeMap::new()) - } -} diff --git a/rust/operator-binary/src/crd/role/mod.rs b/rust/operator-binary/src/crd/role/mod.rs index e08474ed..d607a3b9 100644 --- a/rust/operator-binary/src/crd/role/mod.rs +++ b/rust/operator-binary/src/crd/role/mod.rs @@ -5,27 +5,19 @@ pub mod controller; use std::{borrow::Cow, ops::Deref}; use serde::{Deserialize, Serialize}; -use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ commons::resources::{NoRuntimeLimits, Resources}, - config::{ - fragment::{self, ValidationError}, - merge::Merge, - }, - k8s_openapi::api::core::v1::PodTemplateSpec, - kube::{ResourceExt, runtime::reflector::ObjectRef}, product_logging::spec::ContainerLogConfig, - role_utils::RoleGroupRef, schemars::{self, JsonSchema}, + v2::{config_overrides::KeyValueConfigOverrides, types::kubernetes::ListenerClassName}, }; use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; use crate::{ - config::jvm::{construct_heap_jvm_args, construct_non_heap_jvm_args}, crd::role::{ - broker::{BROKER_PROPERTIES_FILE, BrokerConfig}, + broker::BrokerConfig, commons::{CommonConfig, Storage}, - controller::{CONTROLLER_PROPERTIES_FILE, ControllerConfig}, + controller::ControllerConfig, }, v1alpha1, }; @@ -71,24 +63,6 @@ pub const KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: &str = "listener.security.protoc /// For example: localhost:9092,localhost:9093,localhost:9094. pub const KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS: &str = "controller.quorum.bootstrap.servers"; -#[derive(Snafu, Debug)] -pub enum Error { - #[snafu(display("fragment validation failure"))] - FragmentValidationFailure { source: ValidationError }, - - #[snafu(display("the Kafka role [{role}] is missing from spec"))] - MissingRole { - source: crate::crd::Error, - role: String, - }, - - #[snafu(display("missing role group {rolegroup:?} for role {role:?}"))] - MissingRoleGroup { role: String, rolegroup: String }, - - #[snafu(display("failed to construct JVM arguments"))] - ConstructJvmArguments { source: crate::config::jvm::Error }, -} - #[derive( Clone, Debug, @@ -98,7 +72,9 @@ pub enum Error { Eq, Hash, JsonSchema, + Ord, PartialEq, + PartialOrd, Serialize, EnumString, )] @@ -119,265 +95,15 @@ impl KafkaRole { roles } - /// Metadata about a rolegroup - pub fn rolegroup_ref( - &self, - kafka: &v1alpha1::KafkaCluster, - group_name: impl Into, - ) -> RoleGroupRef { - RoleGroupRef { - cluster: ObjectRef::from_obj(kafka), - role: self.to_string(), - role_group: group_name.into(), - } - } - /// A Kerberos principal has three parts, with the form username/fully.qualified.domain.name@YOUR-REALM.COM. /// but is similar to HBase). pub fn kerberos_service_name(&self) -> &'static str { "kafka" } - - /// Merge the [Broker|Controller]ConfigFragment defaults, role and role group settings. - /// The priority is: default < role config < role_group config - pub fn merged_config( - &self, - kafka: &v1alpha1::KafkaCluster, - rolegroup: &str, - ) -> Result { - match self { - Self::Broker => { - // Initialize the result with all default values as baseline - let default_config = - BrokerConfig::default_config(&kafka.name_any(), &self.to_string()); - - // Retrieve role resource config - let role = kafka.broker_role().with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })?; - - let mut role_config = role.config.config.clone(); - // Retrieve rolegroup specific resource config - let mut role_group_config = role - .role_groups - .get(rolegroup) - .with_context(|| MissingRoleGroupSnafu { - role: self.to_string(), - rolegroup: rolegroup.to_string(), - })? - .config - .config - .clone(); - - // Merge more specific configs into default config - // Hierarchy is: - // 1. RoleGroup - // 2. Role - // 3. Default - role_config.merge(&default_config); - role_group_config.merge(&role_config); - Ok(AnyConfig::Broker( - fragment::validate::(role_group_config) - .context(FragmentValidationFailureSnafu)?, - )) - } - Self::Controller => { - // Initialize the result with all default values as baseline - let default_config = - ControllerConfig::default_config(&kafka.name_any(), &self.to_string()); - - // Retrieve role resource config - let role = kafka.controller_role().with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })?; - - let mut role_config = role.config.config.clone(); - // Retrieve rolegroup specific resource config - let mut role_group_config = role - .role_groups - .get(rolegroup) - .with_context(|| MissingRoleGroupSnafu { - role: self.to_string(), - rolegroup: rolegroup.to_string(), - })? - .config - .config - .clone(); - - // Merge more specific configs into default config - // Hierarchy is: - // 1. RoleGroup - // 2. Role - // 3. Default - role_config.merge(&default_config); - role_group_config.merge(&role_config); - Ok(AnyConfig::Controller( - fragment::validate::(role_group_config) - .context(FragmentValidationFailureSnafu)?, - )) - } - } - } - - pub fn construct_non_heap_jvm_args( - &self, - merged_config: &AnyConfig, - kafka: &v1alpha1::KafkaCluster, - rolegroup: &str, - ) -> Result { - match self { - Self::Broker => construct_non_heap_jvm_args( - merged_config, - kafka.broker_role().with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })?, - rolegroup, - ) - .context(ConstructJvmArgumentsSnafu), - Self::Controller => construct_non_heap_jvm_args( - merged_config, - kafka.controller_role().with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })?, - rolegroup, - ) - .context(ConstructJvmArgumentsSnafu), - } - } - - pub fn construct_heap_jvm_args( - &self, - merged_config: &AnyConfig, - kafka: &v1alpha1::KafkaCluster, - rolegroup: &str, - ) -> Result { - match self { - Self::Broker => construct_heap_jvm_args( - merged_config, - kafka.broker_role().with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })?, - rolegroup, - ) - .context(ConstructJvmArgumentsSnafu), - Self::Controller => construct_heap_jvm_args( - merged_config, - kafka.controller_role().with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })?, - rolegroup, - ) - .context(ConstructJvmArgumentsSnafu), - } - } - - pub fn role_pod_overrides( - &self, - kafka: &v1alpha1::KafkaCluster, - ) -> Result { - let pod_overrides = match self { - Self::Broker => kafka - .broker_role() - .with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })? - .config - .pod_overrides - .clone(), - Self::Controller => kafka - .controller_role() - .with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })? - .config - .pod_overrides - .clone(), - }; - - Ok(pod_overrides) - } - - pub fn role_group_pod_overrides( - &self, - kafka: &v1alpha1::KafkaCluster, - rolegroup: &str, - ) -> Result { - let pod_overrides = match self { - Self::Broker => kafka - .broker_role() - .with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })? - .role_groups - .get(rolegroup) - .with_context(|| MissingRoleGroupSnafu { - role: self.to_string(), - rolegroup: rolegroup.to_string(), - })? - .config - .pod_overrides - .clone(), - Self::Controller => kafka - .controller_role() - .with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })? - .role_groups - .get(rolegroup) - .with_context(|| MissingRoleGroupSnafu { - role: self.to_string(), - rolegroup: rolegroup.to_string(), - })? - .config - .pod_overrides - .clone(), - }; - - Ok(pod_overrides) - } - - pub fn replicas( - &self, - kafka: &v1alpha1::KafkaCluster, - rolegroup: &str, - ) -> Result, Error> { - let replicas = match self { - Self::Broker => { - kafka - .broker_role() - .with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })? - .role_groups - .get(rolegroup) - .with_context(|| MissingRoleGroupSnafu { - role: self.to_string(), - rolegroup: rolegroup.to_string(), - })? - .replicas - } - Self::Controller => { - kafka - .controller_role() - .with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })? - .role_groups - .get(rolegroup) - .with_context(|| MissingRoleGroupSnafu { - role: self.to_string(), - rolegroup: rolegroup.to_string(), - })? - .replicas - } - }; - - Ok(replicas) - } } /// Configuration for a role and rolegroup of an unknown type. -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum AnyConfig { Broker(BrokerConfig), Controller(ControllerConfig), @@ -395,6 +121,14 @@ impl Deref for AnyConfig { } impl AnyConfig { + /// The [`KafkaRole`] this config belongs to. + pub fn kafka_role(&self) -> KafkaRole { + match self { + AnyConfig::Broker(_) => KafkaRole::Broker, + AnyConfig::Controller(_) => KafkaRole::Controller, + } + } + pub fn resources(&self) -> &Resources { match self { AnyConfig::Broker(broker_config) => &broker_config.resources, @@ -413,37 +147,40 @@ impl AnyConfig { } } - pub fn vector_logging(&'_ self) -> Cow<'_, ContainerLogConfig> { - match &self { - AnyConfig::Broker(broker_config) => broker_config - .logging - .for_container(&broker::BrokerContainer::Vector), - AnyConfig::Controller(controller_config) => controller_config - .logging - .for_container(&controller::ControllerContainer::Vector), - } - } - - pub fn vector_logging_enabled(&self) -> bool { + pub fn listener_class(&self) -> Option<&ListenerClassName> { match self { - AnyConfig::Broker(broker_config) => broker_config.logging.enable_vector_agent, - AnyConfig::Controller(controller_config) => { - controller_config.logging.enable_vector_agent - } + AnyConfig::Broker(broker_config) => Some(&broker_config.broker_listener_class), + AnyConfig::Controller(_) => None, } } +} + +/// Merged role/role-group `configOverrides` for a role group of an unknown type. +/// +/// Mirrors [`AnyConfig`] for the override side: broker and controller use distinct +/// override structs, so this enum lets the build layer carry the typed, merged +/// overrides through a single role-agnostic `RoleGroupConfig`. +#[derive(Clone, Debug, PartialEq)] +pub enum AnyConfigOverrides { + Broker(v1alpha1::KafkaBrokerConfigOverrides), + Controller(v1alpha1::KafkaControllerConfigOverrides), +} - pub fn listener_class(&self) -> Option<&String> { +impl AnyConfigOverrides { + /// The merged product config-file overrides (`broker.properties` for brokers, + /// `controller.properties` for controllers). + pub fn config_file_overrides(&self) -> &KeyValueConfigOverrides { match self { - AnyConfig::Broker(broker_config) => Some(&broker_config.broker_listener_class), - AnyConfig::Controller(_) => None, + AnyConfigOverrides::Broker(o) => &o.broker_properties, + AnyConfigOverrides::Controller(o) => &o.controller_properties, } } - pub fn config_file_name(&self) -> &str { + /// The merged `security.properties` overrides (shared by both roles). + pub fn security_properties(&self) -> &KeyValueConfigOverrides { match self { - AnyConfig::Broker(_) => BROKER_PROPERTIES_FILE, - AnyConfig::Controller(_) => CONTROLLER_PROPERTIES_FILE, + AnyConfigOverrides::Broker(o) => &o.security_properties, + AnyConfigOverrides::Controller(o) => &o.security_properties, } } } diff --git a/rust/operator-binary/src/crd/security.rs b/rust/operator-binary/src/crd/security.rs deleted file mode 100644 index 334aeda8..00000000 --- a/rust/operator-binary/src/crd/security.rs +++ /dev/null @@ -1,930 +0,0 @@ -//! A helper module to process Apache Kafka security configuration -//! -//! This module merges the `tls` and `authentication` module and offers better accessibility -//! and helper functions -//! -//! This is required due to overlaps between TLS encryption and e.g. mTLS authentication or Kerberos -use std::collections::BTreeMap; - -use snafu::{ResultExt, Snafu, ensure}; -use stackable_operator::{ - builder::{ - self, - pod::{ - PodBuilder, - container::ContainerBuilder, - volume::{SecretFormat, SecretOperatorVolumeSourceBuilder, VolumeBuilder}, - }, - }, - commons::secret_class::SecretClassVolumeProvisionParts, - crd::authentication::core, - k8s_openapi::api::core::v1::Volume, - shared::time::Duration, -}; - -use super::listener::KafkaListenerProtocol; -use crate::crd::{ - LISTENER_BOOTSTRAP_VOLUME_NAME, LISTENER_BROKER_VOLUME_NAME, STACKABLE_KERBEROS_KRB5_PATH, - STACKABLE_LISTENER_BROKER_DIR, - authentication::ResolvedAuthenticationClasses, - listener::{self, KafkaListenerName, node_address_cmd_env, node_port_cmd_env}, - role::KafkaRole, - tls, v1alpha1, -}; - -#[derive(Snafu, Debug)] -pub enum Error { - #[snafu(display("failed to build the secret operator Volume"))] - SecretVolumeBuild { - source: stackable_operator::builder::pod::volume::SecretOperatorVolumeSourceBuilderError, - }, - - #[snafu(display("failed to add needed volume"))] - AddVolume { source: builder::pod::Error }, - - #[snafu(display("failed to add needed volumeMount"))] - AddVolumeMount { - source: builder::pod::container::Error, - }, - - #[snafu(display("kerberos enablement requires TLS activation"))] - KerberosRequiresTls, - - #[snafu(display("failed to build OPA TLS certificate volume"))] - OpaTlsCertSecretClassVolumeBuild { - source: stackable_operator::builder::pod::volume::SecretOperatorVolumeSourceBuilderError, - }, -} - -/// Helper struct combining TLS settings for server and internal with the resolved AuthenticationClasses -pub struct KafkaTlsSecurity { - resolved_authentication_classes: ResolvedAuthenticationClasses, - internal_secret_class: String, - server_secret_class: Option, - opa_secret_class: Option, -} - -impl KafkaTlsSecurity { - pub const BOOTSTRAP_PORT: u16 = 9094; - // bootstrap: we will have a single named port with different values for - // secure (9095) and insecure (9094). The bootstrap listener is needed to - // be able to expose principals for both the broker and bootstrap in the - // JAAS configuration, so that clients can use both. - pub const BOOTSTRAP_PORT_NAME: &'static str = "bootstrap"; - pub const CLIENT_PORT: u16 = 9092; - // ports - pub const CLIENT_PORT_NAME: &'static str = "kafka"; - // internal - pub const INTERNAL_PORT: u16 = 19092; - // - TLS internal - const INTER_BROKER_LISTENER_NAME: &'static str = "inter.broker.listener.name"; - const OPA_TLS_MOUNT_PATH: &str = "/stackable/tls-opa"; - // opa - const OPA_TLS_VOLUME_NAME: &str = "tls-opa"; - pub const SECURE_BOOTSTRAP_PORT: u16 = 9095; - pub const SECURE_CLIENT_PORT: u16 = 9093; - pub const SECURE_CLIENT_PORT_NAME: &'static str = "kafka-tls"; - pub const SECURE_INTERNAL_PORT: u16 = 19093; - // - TLS global - const SSL_STORE_PASSWORD: &'static str = ""; - const STACKABLE_TLS_KAFKA_INTERNAL_DIR: &'static str = "/stackable/tls-kafka-internal"; - const STACKABLE_TLS_KAFKA_INTERNAL_VOLUME_NAME: &'static str = "tls-kafka-internal"; - const STACKABLE_TLS_KAFKA_SERVER_DIR: &'static str = "/stackable/tls-kafka-server"; - const STACKABLE_TLS_KAFKA_SERVER_VOLUME_NAME: &'static str = "tls-kafka-server"; - // directories - const STACKABLE_TLS_KCAT_DIR: &'static str = "/stackable/tls-kcat"; - const STACKABLE_TLS_KCAT_VOLUME_NAME: &'static str = "tls-kcat"; - - #[cfg(test)] - pub fn new( - resolved_authentication_classes: ResolvedAuthenticationClasses, - internal_secret_class: String, - server_secret_class: Option, - opa_secret_class: Option, - ) -> Self { - Self { - resolved_authentication_classes, - internal_secret_class, - server_secret_class, - opa_secret_class, - } - } - - /// Build a [`KafkaTlsSecurity`] from already-resolved authentication classes. - /// - /// The async retrieval of [`ResolvedAuthenticationClasses`] now happens in the dereference - /// step of the controller; this constructor only reads TLS settings from the spec. - pub fn new_from_kafka_cluster( - kafka: &v1alpha1::KafkaCluster, - resolved_authentication_classes: ResolvedAuthenticationClasses, - opa_secret_class: Option, - ) -> Self { - KafkaTlsSecurity { - resolved_authentication_classes, - internal_secret_class: kafka - .spec - .cluster_config - .tls - .as_ref() - .map(|tls| tls.internal_secret_class.clone()) - .unwrap_or_else(tls::internal_tls_default), - server_secret_class: kafka - .spec - .cluster_config - .tls - .as_ref() - .and_then(|tls| tls.server_secret_class.clone()), - opa_secret_class, - } - } - - /// Check if TLS encryption is enabled. This could be due to: - /// - A provided server `SecretClass` - /// - A provided client `AuthenticationClass` - /// - /// This affects init container commands, Kafka configuration, volume mounts and - /// the Kafka client port - pub fn tls_enabled(&self) -> bool { - // TODO: This must be adapted if other authentication methods are supported and require TLS - self.tls_client_authentication_class().is_some() || self.tls_server_secret_class().is_some() - } - - /// Retrieve an optional TLS secret class for external client -> server communications. - pub fn tls_server_secret_class(&self) -> Option<&str> { - self.server_secret_class.as_deref() - } - - /// Retrieve an optional TLS `AuthenticationClass`. - pub fn tls_client_authentication_class(&self) -> Option<&core::v1alpha1::AuthenticationClass> { - self.resolved_authentication_classes - .get_tls_authentication_class() - } - - /// Retrieve the mandatory internal `SecretClass`. - pub fn tls_internal_secret_class(&self) -> Option<&str> { - if !self.internal_secret_class.is_empty() { - Some(self.internal_secret_class.as_str()) - } else { - None - } - } - - pub fn has_kerberos_enabled(&self) -> bool { - self.kerberos_secret_class().is_some() - } - - fn has_opa_tls_enabled(&self) -> bool { - self.opa_secret_class.is_some() - } - - pub fn copy_opa_tls_cert_command(&self) -> String { - match self.has_opa_tls_enabled() { - true => format!( - "keytool -importcert -file {opa_mount_path}/ca.crt -keystore {tls_dir}/truststore.p12 -storepass '{tls_password}' -alias opa-ca -noprompt", - opa_mount_path = Self::OPA_TLS_MOUNT_PATH, - tls_dir = Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR, - tls_password = Self::SSL_STORE_PASSWORD, - ), - false => "".to_string(), - } - } - - pub fn kerberos_secret_class(&self) -> Option { - if let Some(kerberos) = self - .resolved_authentication_classes - .get_kerberos_authentication_class() - { - match &kerberos.spec.provider { - core::v1alpha1::AuthenticationClassProvider::Kerberos(kerberos) => { - Some(kerberos.kerberos_secret_class.clone()) - } - _ => None, - } - } else { - None - } - } - - pub fn validate_authentication_methods(&self) -> Result<(), Error> { - // Client TLS authentication and Kerberos authentication are mutually - // exclusive, but this has already been checked when checking the - // authentication classes. When users enable Kerberos we require them - // to also enable TLS for a) maximum security and b) to limit the - // number of combinations we need to support. - if self.has_kerberos_enabled() { - ensure!(self.server_secret_class.is_some(), KerberosRequiresTlsSnafu); - } - - Ok(()) - } - - /// Return the Kafka (secure) client port depending on tls or authentication settings. - pub fn client_port(&self) -> u16 { - if self.tls_enabled() { - Self::SECURE_CLIENT_PORT - } else { - Self::CLIENT_PORT - } - } - - pub fn bootstrap_port(&self) -> u16 { - if self.tls_enabled() { - Self::SECURE_BOOTSTRAP_PORT - } else { - Self::BOOTSTRAP_PORT - } - } - - pub fn bootstrap_port_name(&self) -> &str { - Self::BOOTSTRAP_PORT_NAME - } - - /// Return the Kafka (secure) client port name depending on tls or authentication settings. - pub fn client_port_name(&self) -> &str { - if self.tls_enabled() { - Self::SECURE_CLIENT_PORT_NAME - } else { - Self::CLIENT_PORT_NAME - } - } - - /// Return the Kafka (secure) internal port depending on tls settings. - pub fn internal_port(&self) -> u16 { - if self.tls_internal_secret_class().is_some() { - Self::SECURE_INTERNAL_PORT - } else { - Self::INTERNAL_PORT - } - } - - /// Returns the commands for the kcat readiness probe. - pub fn kcat_prober_container_commands(&self) -> Vec { - let mut args = vec![]; - let port = self.client_port(); - - if self.tls_client_authentication_class().is_some() { - args.push("/stackable/kcat".to_string()); - args.push("-b".to_string()); - args.push(format!("localhost:{}", port)); - args.extend(Self::kcat_client_auth_ssl(Self::STACKABLE_TLS_KCAT_DIR)); - args.push("-L".to_string()); - } else if self.has_kerberos_enabled() { - let service_name = KafkaRole::Broker.kerberos_service_name(); - // here we need to specify a shell so that variable substitution will work - // see e.g. https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1ExecAction.md - args.push("/bin/bash".to_string()); - args.push("-x".to_string()); - args.push("-euo".to_string()); - args.push("pipefail".to_string()); - args.push("-c".to_string()); - - // the entire command needs to be subject to the -c directive - // to prevent short-circuiting - let mut bash_args = vec![]; - bash_args.push( - format!( - "export KERBEROS_REALM=$(grep -oP 'default_realm = \\K.*' {});", - STACKABLE_KERBEROS_KRB5_PATH - ) - .to_string(), - ); - bash_args.push( - format!( - "export POD_BROKER_LISTENER_ADDRESS={};", - node_address_cmd_env(STACKABLE_LISTENER_BROKER_DIR) - ) - .to_string(), - ); - bash_args.push( - format!( - "export POD_BROKER_LISTENER_PORT={};", - node_port_cmd_env(STACKABLE_LISTENER_BROKER_DIR, self.client_port_name()) - ) - .to_string(), - ); - bash_args.push("/stackable/kcat".to_string()); - bash_args.push("-b".to_string()); - bash_args.push("$POD_BROKER_LISTENER_ADDRESS:$POD_BROKER_LISTENER_PORT".to_string()); - bash_args.extend(Self::kcat_client_sasl_ssl( - Self::STACKABLE_TLS_KCAT_DIR, - service_name, - )); - bash_args.push("-L".to_string()); - - args.push(bash_args.join(" ")); - } else if self.tls_server_secret_class().is_some() { - args.push("/stackable/kcat".to_string()); - args.push("-b".to_string()); - args.push(format!("localhost:{}", port)); - args.extend(Self::kcat_client_ssl(Self::STACKABLE_TLS_KCAT_DIR)); - args.push("-L".to_string()); - } else { - args.push("/stackable/kcat".to_string()); - args.push("-b".to_string()); - args.push(format!("localhost:{}", port)); - args.push("-L".to_string()); - } - - args - } - - /// Returns a configuration file that can be used by Kafka clients running inside the - /// Kubernetes cluster to connect to the Kafka servers. - pub fn client_properties(&self) -> Vec<(String, Option)> { - let mut props = vec![]; - - if self.tls_client_authentication_class().is_some() { - props.push(( - "security.protocol".to_string(), - Some(KafkaListenerProtocol::Ssl.to_string()), - )); - props.push(("ssl.client.auth".to_string(), Some("required".to_string()))); - props.push(("ssl.keystore.type".to_string(), Some("PKCS12".to_string()))); - props.push(( - "ssl.keystore.location".to_string(), - Some(format!( - "{}/keystore.p12", - Self::STACKABLE_TLS_KAFKA_SERVER_DIR - )), - )); - props.push(( - "ssl.keystore.password".to_string(), - Some(Self::SSL_STORE_PASSWORD.to_string()), - )); - props.push(( - "ssl.truststore.type".to_string(), - Some("PKCS12".to_string()), - )); - props.push(( - "ssl.truststore.location".to_string(), - Some(format!( - "{}/truststore.p12", - Self::STACKABLE_TLS_KAFKA_SERVER_DIR - )), - )); - props.push(( - "ssl.truststore.password".to_string(), - Some(Self::SSL_STORE_PASSWORD.to_string()), - )); - } else if self.has_kerberos_enabled() { - // TODO: to make this configuration file usable out of the box the operator needs to be - // refactored to write out Java jaas files instead of passing command line parameters - // to the Kafka daemon scripts. - // This will simplify the code and the command lines lot. - // It will also make the jaas files reusable by the Kafka shell scripts. - props.push(( - "security.protocol".to_string(), - Some(KafkaListenerProtocol::SaslSsl.to_string()), - )); - props.push(("ssl.keystore.type".to_string(), Some("PKCS12".to_string()))); - props.push(( - "ssl.keystore.location".to_string(), - Some(format!( - "{}/keystore.p12", - Self::STACKABLE_TLS_KAFKA_SERVER_DIR - )), - )); - props.push(( - "ssl.keystore.password".to_string(), - Some(Self::SSL_STORE_PASSWORD.to_string()), - )); - props.push(( - "ssl.truststore.type".to_string(), - Some("PKCS12".to_string()), - )); - props.push(( - "ssl.truststore.location".to_string(), - Some(format!( - "{}/truststore.p12", - Self::STACKABLE_TLS_KAFKA_SERVER_DIR - )), - )); - props.push(( - "ssl.truststore.password".to_string(), - Some(Self::SSL_STORE_PASSWORD.to_string()), - )); - props.push(( - "sasl.enabled.mechanisms".to_string(), - Some("GSSAPI".to_string()), - )); - props.push(( - "sasl.kerberos.service.name".to_string(), - Some(KafkaRole::Broker.kerberos_service_name().to_string()), - )); - props.push(( - "sasl.mechanism.inter.broker.protocol".to_string(), - Some("GSSAPI".to_string()), - )); - props.push(( - "sasl.jaas.config".to_string(), - Some(format!("com.sun.security.auth.module.Krb5LoginModule required useKeyTab=true storeKey=true keyTab=\"{keytab}\" principal=\"{service}/{pod}@{realm}\"", - keytab="/stackable/kerberos/keytab", - service=KafkaRole::Broker.kerberos_service_name(), - pod="todo", - realm="$KERBEROS_REALM")))); - } else if self.tls_server_secret_class().is_some() { - props.push(( - "security.protocol".to_string(), - Some(KafkaListenerProtocol::Ssl.to_string()), - )); - props.push(( - "ssl.truststore.type".to_string(), - Some("PKCS12".to_string()), - )); - props.push(( - "ssl.truststore.location".to_string(), - Some(format!( - "{}/truststore.p12", - Self::STACKABLE_TLS_KAFKA_SERVER_DIR - )), - )); - props.push(( - "ssl.truststore.password".to_string(), - Some(Self::SSL_STORE_PASSWORD.to_string()), - )); - } else { - props.push(( - "security.protocol".to_string(), - Some(KafkaListenerProtocol::Plaintext.to_string()), - )); - } - - props - } - - /// Adds required volumes and volume mounts to the broker pod and container builders - /// depending on the tls and authentication settings. - pub fn add_broker_volume_and_volume_mounts( - &self, - pod_builder: &mut PodBuilder, - cb_kcat_prober: &mut ContainerBuilder, - cb_kafka: &mut ContainerBuilder, - requested_secret_lifetime: &Duration, - ) -> Result<(), Error> { - // add tls (server or client authentication volumes) if required - if let Some(tls_server_secret_class) = self.get_tls_secret_class() { - // We have to mount tls pem files for kcat (the mount can be used directly) - pod_builder - .add_volume(Self::create_kcat_tls_volume( - Self::STACKABLE_TLS_KCAT_VOLUME_NAME, - tls_server_secret_class, - requested_secret_lifetime, - )?) - .context(AddVolumeSnafu)?; - cb_kcat_prober - .add_volume_mount( - Self::STACKABLE_TLS_KCAT_VOLUME_NAME, - Self::STACKABLE_TLS_KCAT_DIR, - ) - .context(AddVolumeMountSnafu)?; - // Keystores fore the kafka container - pod_builder - .add_volume(Self::create_tls_keystore_volume( - Self::STACKABLE_TLS_KAFKA_SERVER_VOLUME_NAME, - tls_server_secret_class, - requested_secret_lifetime, - )?) - .context(AddVolumeSnafu)?; - cb_kafka - .add_volume_mount( - Self::STACKABLE_TLS_KAFKA_SERVER_VOLUME_NAME, - Self::STACKABLE_TLS_KAFKA_SERVER_DIR, - ) - .context(AddVolumeMountSnafu)?; - } - - if let Some(tls_internal_secret_class) = self.tls_internal_secret_class() { - pod_builder - .add_volume(Self::create_tls_keystore_volume( - Self::STACKABLE_TLS_KAFKA_INTERNAL_VOLUME_NAME, - tls_internal_secret_class, - requested_secret_lifetime, - )?) - .context(AddVolumeSnafu)?; - cb_kafka - .add_volume_mount( - Self::STACKABLE_TLS_KAFKA_INTERNAL_VOLUME_NAME, - Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR, - ) - .context(AddVolumeMountSnafu)?; - } - - if let Some(secret_class) = &self.opa_secret_class { - cb_kafka - .add_volume_mount(Self::OPA_TLS_VOLUME_NAME, Self::OPA_TLS_MOUNT_PATH) - .context(AddVolumeMountSnafu)?; - - pod_builder - .add_volume( - VolumeBuilder::new(Self::OPA_TLS_VOLUME_NAME) - .ephemeral( - SecretOperatorVolumeSourceBuilder::new( - secret_class, - // Only the truststore is required to connect to OPA. - SecretClassVolumeProvisionParts::Public, - ) - .build() - .context(OpaTlsCertSecretClassVolumeBuildSnafu)?, - ) - .build(), - ) - .context(AddVolumeSnafu)?; - } - - Ok(()) - } - - /// Adds required volumes and volume mounts to the controller pod and container builders - /// depending on the tls and authentication settings. - pub fn add_controller_volume_and_volume_mounts( - &self, - pod_builder: &mut PodBuilder, - cb_kafka: &mut ContainerBuilder, - requested_secret_lifetime: &Duration, - ) -> Result<(), Error> { - if let Some(tls_internal_secret_class) = self.tls_internal_secret_class() { - pod_builder - .add_volume( - VolumeBuilder::new(Self::STACKABLE_TLS_KAFKA_INTERNAL_VOLUME_NAME) - .ephemeral( - SecretOperatorVolumeSourceBuilder::new( - tls_internal_secret_class, - // Kafka needs both the public certificate and the private key for - // the internal communication. - SecretClassVolumeProvisionParts::PublicPrivate, - ) - .with_pod_scope() - .with_format(SecretFormat::TlsPkcs12) - .with_auto_tls_cert_lifetime(*requested_secret_lifetime) - .build() - .context(SecretVolumeBuildSnafu)?, - ) - .build(), - ) - .context(AddVolumeSnafu)?; - cb_kafka - .add_volume_mount( - Self::STACKABLE_TLS_KAFKA_INTERNAL_VOLUME_NAME, - Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR, - ) - .context(AddVolumeMountSnafu)?; - } - - Ok(()) - } - - /// Returns required Kafka configuration settings for the `broker.properties` file - /// depending on the tls and authentication settings. - pub fn broker_config_settings(&self) -> BTreeMap { - let mut config = BTreeMap::new(); - - // We set either client tls with authentication or client tls without authentication - // If authentication is explicitly required we do not want to have any other CAs to - // be trusted. - if self.tls_client_authentication_class().is_some() - || self.tls_server_secret_class().is_some() - { - config.insert( - KafkaListenerName::Client.listener_ssl_keystore_location(), - format!("{}/keystore.p12", Self::STACKABLE_TLS_KAFKA_SERVER_DIR), - ); - config.insert( - KafkaListenerName::Client.listener_ssl_keystore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Client.listener_ssl_keystore_type(), - "PKCS12".to_string(), - ); - config.insert( - KafkaListenerName::Client.listener_ssl_truststore_location(), - format!("{}/truststore.p12", Self::STACKABLE_TLS_KAFKA_SERVER_DIR), - ); - config.insert( - KafkaListenerName::Client.listener_ssl_truststore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Client.listener_ssl_truststore_type(), - "PKCS12".to_string(), - ); - if self.tls_client_authentication_class().is_some() { - // client auth required - config.insert( - KafkaListenerName::Client.listener_ssl_client_auth(), - "required".to_string(), - ); - } - } - - if self.has_kerberos_enabled() { - // Bootstrap - config.insert( - KafkaListenerName::Bootstrap.listener_ssl_keystore_location(), - format!("{}/keystore.p12", Self::STACKABLE_TLS_KAFKA_SERVER_DIR), - ); - config.insert( - KafkaListenerName::Bootstrap.listener_ssl_keystore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Bootstrap.listener_ssl_keystore_type(), - "PKCS12".to_string(), - ); - config.insert( - KafkaListenerName::Bootstrap.listener_ssl_truststore_location(), - format!("{}/truststore.p12", Self::STACKABLE_TLS_KAFKA_SERVER_DIR), - ); - config.insert( - KafkaListenerName::Bootstrap.listener_ssl_truststore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Bootstrap.listener_ssl_truststore_type(), - "PKCS12".to_string(), - ); - config.insert("sasl.enabled.mechanisms".to_string(), "GSSAPI".to_string()); - config.insert( - "sasl.kerberos.service.name".to_string(), - KafkaRole::Broker.kerberos_service_name().to_string(), - ); - config.insert( - "sasl.mechanism.inter.broker.protocol".to_string(), - "GSSAPI".to_string(), - ); - tracing::debug!("Kerberos configs added: [{:#?}]", config); - } - - // Internal TLS - if self.tls_internal_secret_class().is_some() { - // BROKERS - config.insert( - KafkaListenerName::Internal.listener_ssl_keystore_location(), - format!("{}/keystore.p12", Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR), - ); - config.insert( - KafkaListenerName::Internal.listener_ssl_keystore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Internal.listener_ssl_keystore_type(), - "PKCS12".to_string(), - ); - config.insert( - KafkaListenerName::Internal.listener_ssl_truststore_location(), - format!("{}/truststore.p12", Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR), - ); - config.insert( - KafkaListenerName::Internal.listener_ssl_truststore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Internal.listener_ssl_truststore_type(), - "PKCS12".to_string(), - ); - // CONTROLLERS - config.insert( - KafkaListenerName::Controller.listener_ssl_keystore_location(), - format!("{}/keystore.p12", Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR), - ); - config.insert( - KafkaListenerName::Controller.listener_ssl_keystore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Controller.listener_ssl_keystore_type(), - "PKCS12".to_string(), - ); - config.insert( - KafkaListenerName::Controller.listener_ssl_truststore_location(), - format!("{}/truststore.p12", Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR), - ); - config.insert( - KafkaListenerName::Controller.listener_ssl_truststore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Controller.listener_ssl_truststore_type(), - "PKCS12".to_string(), - ); - // client auth required - config.insert( - KafkaListenerName::Internal.listener_ssl_client_auth(), - "required".to_string(), - ); - } - - //OPA Tls - if self.opa_secret_class.is_some() { - config.insert( - "opa.authorizer.truststore.path".to_string(), - format!("{}/truststore.p12", Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR), - ); - config.insert( - "opa.authorizer.truststore.password".to_string(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - "opa.authorizer.truststore.type".to_string(), - "PKCS12".to_string(), - ); - } - - // common - config.insert( - Self::INTER_BROKER_LISTENER_NAME.to_string(), - listener::KafkaListenerName::Internal.to_string(), - ); - - config - } - - /// Returns required Kafka configuration settings for the `controller.properties` file - /// depending on the tls and authentication settings. - pub fn controller_config_settings(&self) -> BTreeMap { - let mut config = BTreeMap::new(); - - if self.tls_client_authentication_class().is_some() - || self.tls_internal_secret_class().is_some() - { - config.insert( - KafkaListenerName::Controller.listener_ssl_keystore_location(), - format!("{}/keystore.p12", Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR), - ); - config.insert( - KafkaListenerName::Controller.listener_ssl_keystore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Controller.listener_ssl_keystore_type(), - "PKCS12".to_string(), - ); - config.insert( - KafkaListenerName::Controller.listener_ssl_truststore_location(), - format!("{}/truststore.p12", Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR), - ); - config.insert( - KafkaListenerName::Controller.listener_ssl_truststore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Controller.listener_ssl_truststore_type(), - "PKCS12".to_string(), - ); - - // The TLS properties for the internal broker listener are needed by the Kraft controllers - // too during metadata migration from ZooKeeper to Kraft mode. - config.insert( - KafkaListenerName::Internal.listener_ssl_keystore_location(), - format!("{}/keystore.p12", Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR), - ); - config.insert( - KafkaListenerName::Internal.listener_ssl_keystore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Internal.listener_ssl_keystore_type(), - "PKCS12".to_string(), - ); - config.insert( - KafkaListenerName::Internal.listener_ssl_truststore_location(), - format!("{}/truststore.p12", Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR), - ); - config.insert( - KafkaListenerName::Internal.listener_ssl_truststore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Internal.listener_ssl_truststore_type(), - "PKCS12".to_string(), - ); - // We set either client tls with authentication or client tls without authentication - // If authentication is explicitly required we do not want to have any other CAs to - // be trusted. - if self.tls_client_authentication_class().is_some() { - // client auth required - config.insert( - KafkaListenerName::Controller.listener_ssl_client_auth(), - "required".to_string(), - ); - } - } - - // Kerberos - if self.has_kerberos_enabled() { - config.insert("sasl.enabled.mechanisms".to_string(), "GSSAPI".to_string()); - config.insert( - "sasl.kerberos.service.name".to_string(), - KafkaRole::Controller.kerberos_service_name().to_string(), - ); - config.insert( - "sasl.mechanism.inter.broker.protocol".to_string(), - "GSSAPI".to_string(), - ); - tracing::debug!("Kerberos configs added: [{:#?}]", config); - } - - config - } - - /// Returns the `SecretClass` provided in a `AuthenticationClass` for TLS. - fn get_tls_secret_class(&self) -> Option<&String> { - self.resolved_authentication_classes - .get_tls_authentication_class() - .and_then(|auth_class| match &auth_class.spec.provider { - core::v1alpha1::AuthenticationClassProvider::Tls(tls) => { - tls.client_cert_secret_class.as_ref() - } - _ => None, - }) - .or(self.server_secret_class.as_ref()) - } - - /// Creates ephemeral volumes to mount the `SecretClass` into the Pods for kcat client - fn create_kcat_tls_volume( - volume_name: &str, - secret_class_name: &str, - requested_secret_lifetime: &Duration, - ) -> Result { - Ok(VolumeBuilder::new(volume_name) - .ephemeral( - SecretOperatorVolumeSourceBuilder::new( - secret_class_name, - // Both the public certificate and the private key are required for the kcat - // client authentication. - SecretClassVolumeProvisionParts::PublicPrivate, - ) - .with_pod_scope() - .with_format(SecretFormat::TlsPem) - .with_auto_tls_cert_lifetime(*requested_secret_lifetime) - .build() - .context(SecretVolumeBuildSnafu)?, - ) - .build()) - } - - /// Creates ephemeral volumes to mount the `SecretClass` into the Pods as keystores - fn create_tls_keystore_volume( - volume_name: &str, - secret_class_name: &str, - requested_secret_lifetime: &Duration, - ) -> Result { - Ok(VolumeBuilder::new(volume_name) - .ephemeral( - SecretOperatorVolumeSourceBuilder::new( - secret_class_name, - // Both the keystore and truststore are required for keystore volume. - SecretClassVolumeProvisionParts::PublicPrivate, - ) - .with_pod_scope() - .with_listener_volume_scope(LISTENER_BROKER_VOLUME_NAME) - .with_listener_volume_scope(LISTENER_BOOTSTRAP_VOLUME_NAME) - .with_format(SecretFormat::TlsPkcs12) - .with_auto_tls_cert_lifetime(*requested_secret_lifetime) - .build() - .context(SecretVolumeBuildSnafu)?, - ) - .build()) - } - - fn kcat_client_auth_ssl(cert_directory: &str) -> Vec { - vec![ - "-X".to_string(), - "security.protocol=SSL".to_string(), - "-X".to_string(), - format!("ssl.key.location={cert_directory}/tls.key"), - "-X".to_string(), - format!("ssl.certificate.location={cert_directory}/tls.crt"), - "-X".to_string(), - format!("ssl.ca.location={cert_directory}/ca.crt"), - ] - } - - fn kcat_client_ssl(cert_directory: &str) -> Vec { - vec![ - "-X".to_string(), - "security.protocol=SSL".to_string(), - "-X".to_string(), - format!("ssl.ca.location={cert_directory}/ca.crt"), - ] - } - - fn kcat_client_sasl_ssl(cert_directory: &str, service_name: &str) -> Vec { - vec![ - "-X".to_string(), - "security.protocol=SASL_SSL".to_string(), - "-X".to_string(), - format!("ssl.ca.location={cert_directory}/ca.crt"), - "-X".to_string(), - "sasl.kerberos.keytab=/stackable/kerberos/keytab".to_string(), - "-X".to_string(), - "sasl.mechanism=GSSAPI".to_string(), - "-X".to_string(), - format!("sasl.kerberos.service.name={service_name}"), - "-X".to_string(), - format!( - "sasl.kerberos.principal={service_name}/$POD_BROKER_LISTENER_ADDRESS@$KERBEROS_REALM" - ), - ] - } -} diff --git a/rust/operator-binary/src/crd/tls.rs b/rust/operator-binary/src/crd/tls.rs index 94843601..d899eef6 100644 --- a/rust/operator-binary/src/crd/tls.rs +++ b/rust/operator-binary/src/crd/tls.rs @@ -1,5 +1,10 @@ +use std::str::FromStr; + use serde::{Deserialize, Serialize}; -use stackable_operator::schemars::{self, JsonSchema}; +use stackable_operator::{ + schemars::{self, JsonSchema}, + v2::types::kubernetes::SecretClassName, +}; const TLS_DEFAULT_SECRET_CLASS: &str = "tls"; @@ -13,8 +18,9 @@ pub struct KafkaTls { /// - Which ca.crt to use when validating the other brokers /// /// Defaults to `tls` + /// Set to `null` to disable internal TLS (resulting in a plaintext cluster). #[serde(default = "internal_tls_default")] - pub internal_secret_class: String, + pub internal_secret_class: Option, /// The [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass.html) to use for /// client connections. This setting controls: /// - If TLS encryption is used at all @@ -25,7 +31,7 @@ pub struct KafkaTls { default = "server_tls_default", skip_serializing_if = "Option::is_none" )] - pub server_secret_class: Option, + pub server_secret_class: Option, } /// Default TLS settings. @@ -37,12 +43,18 @@ pub fn default_kafka_tls() -> Option { }) } +/// The `tls` default secret class as a typed name. +fn default_secret_class() -> SecretClassName { + SecretClassName::from_str(TLS_DEFAULT_SECRET_CLASS) + .expect("the default secret class name is valid") +} + /// Helper methods to provide defaults in the CRDs and tests -pub fn internal_tls_default() -> String { - TLS_DEFAULT_SECRET_CLASS.into() +pub fn internal_tls_default() -> Option { + Some(default_secret_class()) } /// Helper methods to provide defaults in the CRDs and tests -pub fn server_tls_default() -> Option { - Some(TLS_DEFAULT_SECRET_CLASS.into()) +pub fn server_tls_default() -> Option { + Some(default_secret_class()) } diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index c074f8af..7a97394a 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -40,15 +40,8 @@ use crate::{ webhooks::conversion::create_webhook_server, }; -mod config; mod controller; mod crd; -mod discovery; -mod kerberos; -mod operations; -mod product_logging; -mod resource; -mod utils; mod webhooks; mod built_info { @@ -80,9 +73,9 @@ async fn main() -> anyhow::Result<()> { RunArguments { operator_environment, watch_namespace, - product_config, maintenance, common, + .. }, .. }) => { @@ -127,11 +120,6 @@ async fn main() -> anyhow::Result<()> { .run(sigterm_watcher.handle()) .map_err(|err| anyhow!(err).context("failed to run webhook server")); - let product_config = product_config.load(&[ - "deploy/config-spec/properties.yaml", - "/etc/stackable/kafka-operator/config-spec/properties.yaml", - ])?; - let event_recorder = Arc::new(Recorder::new( client.as_kube_client(), Reporter { @@ -188,7 +176,6 @@ async fn main() -> anyhow::Result<()> { Arc::new(controller::Ctx { client: client.clone(), operator_environment, - product_config, }), ) // We can let the reporting happen in the background @@ -230,9 +217,16 @@ fn references_config_map( return false; }; - kafka.spec.cluster_config.zookeeper_config_map_name == Some(config_map.name_any()) + let config_map_name = config_map.name_any(); + + kafka + .spec + .cluster_config + .zookeeper_config_map_name + .as_ref() + .is_some_and(|name| name.to_string() == config_map_name) || match &kafka.spec.cluster_config.authorization.opa { - Some(opa_config) => opa_config.config_map_name == config_map.name_any(), + Some(opa_config) => opa_config.config_map_name == config_map_name, None => false, } } diff --git a/rust/operator-binary/src/operations/mod.rs b/rust/operator-binary/src/operations/mod.rs deleted file mode 100644 index 92ca2ec7..00000000 --- a/rust/operator-binary/src/operations/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod graceful_shutdown; -pub mod pdb; diff --git a/rust/operator-binary/src/operations/pdb.rs b/rust/operator-binary/src/operations/pdb.rs deleted file mode 100644 index e42888ca..00000000 --- a/rust/operator-binary/src/operations/pdb.rs +++ /dev/null @@ -1,69 +0,0 @@ -use snafu::{ResultExt, Snafu}; -use stackable_operator::{ - builder::pdb::PodDisruptionBudgetBuilder, client::Client, cluster_resources::ClusterResources, - commons::pdb::PdbConfig, kube::ResourceExt, -}; - -use crate::{ - controller::KAFKA_CONTROLLER_NAME, - crd::{APP_NAME, OPERATOR_NAME, role::KafkaRole, v1alpha1}, -}; - -#[derive(Snafu, Debug)] -pub enum Error { - #[snafu(display("Cannot create PodDisruptionBudget for role [{role}]"))] - CreatePdb { - source: stackable_operator::builder::pdb::Error, - role: String, - }, - #[snafu(display("Cannot apply PodDisruptionBudget [{name}]"))] - ApplyPdb { - source: stackable_operator::cluster_resources::Error, - name: String, - }, -} - -pub async fn add_pdbs( - pdb: &PdbConfig, - kafka: &v1alpha1::KafkaCluster, - role: &KafkaRole, - client: &Client, - cluster_resources: &mut ClusterResources<'_>, -) -> Result<(), Error> { - if !pdb.enabled { - return Ok(()); - } - let max_unavailable = pdb.max_unavailable.unwrap_or(match role { - KafkaRole::Broker => max_unavailable_brokers(), - KafkaRole::Controller => max_unavailable_controllers(), - }); - let pdb = PodDisruptionBudgetBuilder::new_with_role( - kafka, - APP_NAME, - &role.to_string(), - OPERATOR_NAME, - KAFKA_CONTROLLER_NAME, - ) - .with_context(|_| CreatePdbSnafu { - role: role.to_string(), - })? - .with_max_unavailable(max_unavailable) - .build(); - let pdb_name = pdb.name_any(); - cluster_resources - .add(client, pdb) - .await - .with_context(|_| ApplyPdbSnafu { name: pdb_name })?; - - Ok(()) -} - -fn max_unavailable_brokers() -> u16 { - // We can not make any assumptions about topic replication factors. - 1 -} - -fn max_unavailable_controllers() -> u16 { - // TODO: what do we want here? - 1 -} diff --git a/rust/operator-binary/src/product_logging.rs b/rust/operator-binary/src/product_logging.rs deleted file mode 100644 index 8336f5f7..00000000 --- a/rust/operator-binary/src/product_logging.rs +++ /dev/null @@ -1,152 +0,0 @@ -use std::{borrow::Cow, fmt::Display}; - -use stackable_operator::{ - builder::configmap::ConfigMapBuilder, - memory::{BinaryMultiple, MemoryQuantity}, - product_logging::{ - self, - spec::{ContainerLogConfig, ContainerLogConfigChoice}, - }, - role_utils::RoleGroupRef, -}; - -use crate::crd::{ - role::{AnyConfig, broker::BrokerContainer, controller::ControllerContainer}, - v1alpha1, -}; - -pub const BROKER_ID_POD_MAP_DIR: &str = "/stackable/broker-id-pod-map"; -pub const STACKABLE_LOG_CONFIG_DIR: &str = "/stackable/log_config"; -pub const STACKABLE_LOG_DIR: &str = "/stackable/log"; -// log4j -pub const LOG4J_CONFIG_FILE: &str = "log4j.properties"; -pub const KAFKA_LOG4J_FILE: &str = "kafka.log4j.xml"; -// log4j2 -pub const LOG4J2_CONFIG_FILE: &str = "log4j2.properties"; -pub const KAFKA_LOG4J2_FILE: &str = "kafka.log4j2.xml"; -// max size -pub const MAX_KAFKA_LOG_FILES_SIZE: MemoryQuantity = MemoryQuantity { - value: 10.0, - unit: BinaryMultiple::Mebi, -}; - -const CONSOLE_CONVERSION_PATTERN_LOG4J: &str = "[%d] %p %m (%c)%n"; -const CONSOLE_CONVERSION_PATTERN_LOG4J2: &str = "%d{ISO8601} %p [%t] %c - %m%n"; - -pub fn kafka_log_opts(product_version: &str) -> String { - if product_version.starts_with("3.") { - format!("-Dlog4j.configuration=file:{STACKABLE_LOG_CONFIG_DIR}/{LOG4J_CONFIG_FILE}") - } else { - format!("-Dlog4j2.configurationFile=file:{STACKABLE_LOG_CONFIG_DIR}/{LOG4J2_CONFIG_FILE}") - } -} - -pub fn kafka_log_opts_env_var() -> String { - "KAFKA_LOG4J_OPTS".to_string() -} - -/// Extend the role group ConfigMap with logging and Vector configurations -pub fn extend_role_group_config_map( - product_version: &str, - rolegroup: &RoleGroupRef, - merged_config: &AnyConfig, - cm_builder: &mut ConfigMapBuilder, -) { - let container_name = match merged_config { - AnyConfig::Broker(_) => BrokerContainer::Kafka.to_string(), - AnyConfig::Controller(_) => ControllerContainer::Kafka.to_string(), - }; - - // Starting with Kafka 4.0, log4j2 is used instead of log4j. - match product_version.starts_with("3.") { - true => add_log4j_config_if_automatic( - cm_builder, - Some(merged_config.kafka_logging()), - LOG4J_CONFIG_FILE, - container_name, - KAFKA_LOG4J_FILE, - MAX_KAFKA_LOG_FILES_SIZE, - ), - false => add_log4j2_config_if_automatic( - cm_builder, - Some(merged_config.kafka_logging()), - LOG4J2_CONFIG_FILE, - container_name, - KAFKA_LOG4J2_FILE, - MAX_KAFKA_LOG_FILES_SIZE, - ), - } - - let vector_log_config = merged_config.vector_logging(); - let vector_log_config = if let ContainerLogConfig { - choice: Some(ContainerLogConfigChoice::Automatic(log_config)), - } = &*vector_log_config - { - Some(log_config) - } else { - None - }; - - if merged_config.vector_logging_enabled() { - cm_builder.add_data( - product_logging::framework::VECTOR_CONFIG_FILE, - product_logging::framework::create_vector_config(rolegroup, vector_log_config), - ); - } -} - -fn add_log4j_config_if_automatic( - cm_builder: &mut ConfigMapBuilder, - log_config: Option>, - log_config_file: &str, - container_name: impl Display, - log_file: &str, - max_log_file_size: MemoryQuantity, -) { - if let Some(ContainerLogConfig { - choice: Some(ContainerLogConfigChoice::Automatic(log_config)), - }) = log_config.as_deref() - { - cm_builder.add_data( - log_config_file, - product_logging::framework::create_log4j_config( - &format!("{STACKABLE_LOG_DIR}/{container_name}"), - log_file, - max_log_file_size - .scale_to(BinaryMultiple::Mebi) - .floor() - .value as u32, - CONSOLE_CONVERSION_PATTERN_LOG4J, - log_config, - ), - ); - } -} - -fn add_log4j2_config_if_automatic( - cm_builder: &mut ConfigMapBuilder, - log_config: Option>, - log_config_file: &str, - container_name: impl Display, - log_file: &str, - max_log_file_size: MemoryQuantity, -) { - if let Some(ContainerLogConfig { - choice: Some(ContainerLogConfigChoice::Automatic(log_config)), - }) = log_config.as_deref() - { - cm_builder.add_data( - log_config_file, - product_logging::framework::create_log4j2_config( - &format!("{STACKABLE_LOG_DIR}/{container_name}",), - log_file, - max_log_file_size - .scale_to(BinaryMultiple::Mebi) - .floor() - .value as u32, - CONSOLE_CONVERSION_PATTERN_LOG4J2, - log_config, - ), - ); - } -} diff --git a/rust/operator-binary/src/resource/configmap.rs b/rust/operator-binary/src/resource/configmap.rs deleted file mode 100644 index 473120b3..00000000 --- a/rust/operator-binary/src/resource/configmap.rs +++ /dev/null @@ -1,421 +0,0 @@ -use std::{ - collections::{BTreeMap, HashMap}, - str::FromStr, -}; - -use indoc::formatdoc; -use product_config::{types::PropertyNameKind, writer::to_java_properties_string}; -use snafu::{OptionExt, ResultExt, Snafu}; -use stackable_operator::{ - builder::{configmap::ConfigMapBuilder, meta::ObjectMetaBuilder}, - commons::product_image_selection::ResolvedProductImage, - k8s_openapi::api::core::v1::ConfigMap, - role_utils::RoleGroupRef, -}; - -use crate::{ - controller::KAFKA_CONTROLLER_NAME, - crd::{ - JVM_SECURITY_PROPERTIES_FILE, KafkaPodDescriptor, MetadataManager, - STACKABLE_LISTENER_BOOTSTRAP_DIR, STACKABLE_LISTENER_BROKER_DIR, - listener::{KafkaListenerConfig, KafkaListenerName, node_address_cmd}, - role::{ - AnyConfig, KAFKA_ADVERTISED_LISTENERS, KAFKA_BROKER_ID, - KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS, KAFKA_LISTENER_SECURITY_PROTOCOL_MAP, - KAFKA_LISTENERS, KAFKA_LOG_DIRS, KAFKA_NODE_ID, KAFKA_PROCESS_ROLES, KafkaRole, - }, - security::KafkaTlsSecurity, - v1alpha1, - }, - operations::graceful_shutdown::graceful_shutdown_config_properties, - product_logging::extend_role_group_config_map, - utils::build_recommended_labels, -}; - -#[derive(Snafu, Debug)] -pub enum Error { - #[snafu(display("invalid metadata manager"))] - InvalidMetadataManager { source: crate::crd::Error }, - - #[snafu(display("failed to build ConfigMap for {}", rolegroup))] - BuildRoleGroupConfig { - source: stackable_operator::builder::configmap::Error, - rolegroup: RoleGroupRef, - }, - - #[snafu(display( - "failed to serialize [{JVM_SECURITY_PROPERTIES_FILE}] for {}", - rolegroup - ))] - JvmSecurityProperties { - source: product_config::writer::PropertiesWriterError, - rolegroup: String, - }, - - #[snafu(display("failed to build Metadata"))] - MetadataBuild { - source: stackable_operator::builder::meta::Error, - }, - - #[snafu(display("object is missing metadata to build owner reference"))] - ObjectMissingMetadataForOwnerRef { - source: stackable_operator::builder::meta::Error, - }, - - #[snafu(display("failed to serialize config for {rolegroup}"))] - SerializeConfig { - source: product_config::writer::PropertiesWriterError, - rolegroup: RoleGroupRef, - }, - - #[snafu(display("no Kraft controllers found to build"))] - NoKraftControllersFound, - - #[snafu(display("unknown Kafka role [{name}]"))] - UnknownKafkaRole { - source: strum::ParseError, - name: String, - }, - - #[snafu(display("failed to build jaas configuration file for {rolegroup}"))] - BuildJaasConfig { rolegroup: String }, -} - -/// The rolegroup [`ConfigMap`] configures the rolegroup based on the configuration given by the administrator -#[allow(clippy::too_many_arguments)] -pub fn build_rolegroup_config_map( - kafka: &v1alpha1::KafkaCluster, - resolved_product_image: &ResolvedProductImage, - kafka_security: &KafkaTlsSecurity, - rolegroup: &RoleGroupRef, - rolegroup_config: &HashMap>, - merged_config: &AnyConfig, - listener_config: &KafkaListenerConfig, - pod_descriptors: &[KafkaPodDescriptor], - opa_connect_string: Option<&str>, -) -> Result { - let kafka_config_file_name = merged_config.config_file_name(); - - let metadata_manager = kafka - .effective_metadata_manager() - .context(InvalidMetadataManagerSnafu)?; - - let mut kafka_config = server_properties_file( - metadata_manager == MetadataManager::KRaft, - &rolegroup.role, - pod_descriptors, - listener_config, - opa_connect_string, - kafka - .spec - .cluster_config - .broker_id_pod_config_map_name - .is_some(), - )?; - - match merged_config { - AnyConfig::Broker(_) => kafka_config.extend(kafka_security.broker_config_settings()), - AnyConfig::Controller(_) => { - kafka_config.extend(kafka_security.controller_config_settings()) - } - } - - kafka_config.extend(graceful_shutdown_config_properties()); - - // Need to call this to get configOverrides :( - kafka_config.extend( - rolegroup_config - .get(&PropertyNameKind::File(kafka_config_file_name.to_string())) - .cloned() - .unwrap_or_default(), - ); - - let kafka_config = kafka_config - .into_iter() - .map(|(k, v)| (k, Some(v))) - .collect::>(); - - let jvm_sec_props: BTreeMap> = rolegroup_config - .get(&PropertyNameKind::File( - JVM_SECURITY_PROPERTIES_FILE.to_string(), - )) - .cloned() - .unwrap_or_default() - .into_iter() - .map(|(k, v)| (k, Some(v))) - .collect(); - - let mut cm_builder = ConfigMapBuilder::new(); - cm_builder - .metadata( - ObjectMetaBuilder::new() - .name_and_namespace(kafka) - .name(rolegroup.object_name()) - .ownerreference_from_resource(kafka, None, Some(true)) - .context(ObjectMissingMetadataForOwnerRefSnafu)? - .with_recommended_labels(&build_recommended_labels( - kafka, - KAFKA_CONTROLLER_NAME, - &resolved_product_image.app_version_label_value, - &rolegroup.role, - &rolegroup.role_group, - )) - .context(MetadataBuildSnafu)? - .build(), - ) - .add_data( - kafka_config_file_name, - to_java_properties_string(kafka_config.iter().map(|(k, v)| (k, v))).with_context( - |_| SerializeConfigSnafu { - rolegroup: rolegroup.clone(), - }, - )?, - ) - .add_data( - JVM_SECURITY_PROPERTIES_FILE, - to_java_properties_string(jvm_sec_props.iter()).with_context(|_| { - JvmSecurityPropertiesSnafu { - rolegroup: rolegroup.role_group.clone(), - } - })?, - ) - .add_data( - "client.properties", - to_java_properties_string( - kafka_security - .client_properties() - .iter() - .map(|(k, v)| (k, v)), - ) - .with_context(|_| JvmSecurityPropertiesSnafu { - rolegroup: rolegroup.role_group.clone(), - })?, - ) - // This file contains the JAAS configuration for Kerberos authentication - // It has the ".properties" extension but is not a Java properties file. - // It is processed by `config-utils` to substitute "env:" and "file:" variables - // and this tool currently doesn't support the JAAS login configuration format. - .add_data( - "jaas.properties", - jaas_config_file(kafka_security.has_kerberos_enabled()), - ); - - tracing::debug!(?kafka_config, "Applied kafka config"); - tracing::debug!(?jvm_sec_props, "Applied JVM config"); - - extend_role_group_config_map( - &resolved_product_image.product_version, - rolegroup, - merged_config, - &mut cm_builder, - ); - - cm_builder - .build() - .with_context(|_| BuildRoleGroupConfigSnafu { - rolegroup: rolegroup.clone(), - }) -} - -// Generate the content of both broker.properties and controller.properties files. -fn server_properties_file( - kraft_mode: bool, - role: &str, - pod_descriptors: &[KafkaPodDescriptor], - listener_config: &KafkaListenerConfig, - opa_connect_string: Option<&str>, - disable_broker_id_generation: bool, -) -> Result, Error> { - let kraft_controllers = kraft_controllers(pod_descriptors); - - let role = KafkaRole::from_str(role).context(UnknownKafkaRoleSnafu { - name: role.to_string(), - })?; - - match role { - KafkaRole::Controller => { - let kraft_controllers = kraft_controllers.context(NoKraftControllersFoundSnafu)?; - - let mut result = BTreeMap::from([ - ( - KAFKA_LOG_DIRS.to_string(), - "/stackable/data/kraft".to_string(), - ), - (KAFKA_PROCESS_ROLES.to_string(), role.to_string()), - ( - "controller.listener.names".to_string(), - KafkaListenerName::Controller.to_string(), - ), - ( - KAFKA_NODE_ID.to_string(), - "${env:REPLICA_ID}".to_string(), - ), - ( - KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS.to_string(), - kraft_controllers.clone(), - ), - ( - KAFKA_LISTENERS.to_string(), - "CONTROLLER://${env:POD_NAME}.${env:ROLEGROUP_HEADLESS_SERVICE_NAME}.${env:NAMESPACE}.svc.${env:CLUSTER_DOMAIN}:${env:KAFKA_CLIENT_PORT}".to_string(), - ), - ( - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP.to_string(), - listener_config - .listener_security_protocol_map_for_controller()), - ]); - - result.insert( - "inter.broker.listener.name".to_string(), - KafkaListenerName::Internal.to_string(), - ); - - // The ZooKeeper connection is needed for migration from ZooKeeper to KRaft mode. - // It is not needed once the controller is fully running in KRaft mode. - if !kraft_mode { - result.insert( - "zookeeper.connect".to_string(), - "${env:ZOOKEEPER}".to_string(), - ); - } - Ok(result) - } - KafkaRole::Broker => { - let mut result = BTreeMap::from([ - ( - KAFKA_LOG_DIRS.to_string(), - "/stackable/data/topicdata".to_string(), - ), - (KAFKA_LISTENERS.to_string(), listener_config.listeners()), - ( - KAFKA_ADVERTISED_LISTENERS.to_string(), - listener_config.advertised_listeners(), - ), - ( - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP.to_string(), - listener_config.listener_security_protocol_map(), - ), - ( - "inter.broker.listener.name".to_string(), - KafkaListenerName::Internal.to_string(), - ), - ]); - - if kraft_mode { - let kraft_controllers = kraft_controllers.context(NoKraftControllersFoundSnafu)?; - - // Running in KRaft mode - result.extend([ - ( - "broker.id.generation.enable".to_string(), - "false".to_string(), - ), - (KAFKA_NODE_ID.to_string(), "${env:REPLICA_ID}".to_string()), - ( - KAFKA_PROCESS_ROLES.to_string(), - KafkaRole::Broker.to_string(), - ), - ( - "controller.listener.names".to_string(), - KafkaListenerName::Controller.to_string(), - ), - ( - KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS.to_string(), - kraft_controllers.clone(), - ), - ]); - } else { - // Running with ZooKeeper enabled - result.extend([( - "zookeeper.connect".to_string(), - "${env:ZOOKEEPER}".to_string(), - )]); - // We are in zookeeper mode and the user has defined a broker id mapping - // so we disable automatic id generation. - // This check ensures that existing clusters running in ZooKeeper mode do not - // suddenly break after the introduction of this change. - if disable_broker_id_generation { - result.extend([ - ( - "broker.id.generation.enable".to_string(), - "false".to_string(), - ), - (KAFKA_BROKER_ID.to_string(), "${env:REPLICA_ID}".to_string()), - ]); - } - } - - // Enable OPA authorization - if opa_connect_string.is_some() { - result.extend([ - ( - "authorizer.class.name".to_string(), - "org.openpolicyagent.kafka.OpaAuthorizer".to_string(), - ), - ( - "opa.authorizer.metrics.enabled".to_string(), - "true".to_string(), - ), - ( - "opa.authorizer.url".to_string(), - opa_connect_string.unwrap_or_default().to_string(), - ), - ]); - } - - Ok(result) - } - } -} - -fn kraft_controllers(pod_descriptors: &[KafkaPodDescriptor]) -> Option { - let result = pod_descriptors - .iter() - .filter(|pd| pd.role == KafkaRole::Controller.to_string()) - .map(|desc| { - format!( - "{fqdn}:{client_port}", - fqdn = desc.fqdn(), - client_port = desc.client_port - ) - }) - .collect::>() - .join(","); - - if result.is_empty() { - None - } else { - Some(result) - } -} - -// Generate JAAS configuration file for Kerberos authentication -// or an empty string if Kerberos is not enabled. -// See https://docs.oracle.com/javase/8/docs/technotes/guides/security/jgss/tutorials/LoginConfigFile.html -fn jaas_config_file(is_kerberos_enabled: bool) -> String { - match is_kerberos_enabled { - false => String::new(), - true => formatdoc! {" - bootstrap.KafkaServer {{ - com.sun.security.auth.module.Krb5LoginModule required - useKeyTab=true - storeKey=true - isInitiator=false - keyTab=\"/stackable/kerberos/keytab\" - principal=\"kafka/{bootstrap_address}@${{env:KERBEROS_REALM}}\"; - }}; - - client.KafkaServer {{ - com.sun.security.auth.module.Krb5LoginModule required - useKeyTab=true - storeKey=true - isInitiator=false - keyTab=\"/stackable/kerberos/keytab\" - principal=\"kafka/{broker_address}@${{env:KERBEROS_REALM}}\"; - }}; - - ", - bootstrap_address = node_address_cmd(STACKABLE_LISTENER_BOOTSTRAP_DIR), - broker_address = node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), - }, - } -} diff --git a/rust/operator-binary/src/resource/listener.rs b/rust/operator-binary/src/resource/listener.rs deleted file mode 100644 index 4afde134..00000000 --- a/rust/operator-binary/src/resource/listener.rs +++ /dev/null @@ -1,76 +0,0 @@ -use snafu::{ResultExt, Snafu}; -use stackable_operator::{ - builder::meta::ObjectMetaBuilder, commons::product_image_selection::ResolvedProductImage, - crd::listener, role_utils::RoleGroupRef, -}; - -use crate::{ - controller::KAFKA_CONTROLLER_NAME, - crd::{role::broker::BrokerConfig, security::KafkaTlsSecurity, v1alpha1}, - utils::build_recommended_labels, -}; - -#[derive(Snafu, Debug)] -pub enum Error { - #[snafu(display("failed to build Metadata"))] - MetadataBuild { - source: stackable_operator::builder::meta::Error, - }, - - #[snafu(display("object is missing metadata to build owner reference"))] - ObjectMissingMetadataForOwnerRef { - source: stackable_operator::builder::meta::Error, - }, -} - -/// Kafka clients will use the load-balanced bootstrap listener to get a list of broker addresses and will use those to -/// transmit data to the correct broker. -// TODO (@NickLarsenNZ): Move shared functionality to stackable-operator -pub fn build_broker_rolegroup_bootstrap_listener( - kafka: &v1alpha1::KafkaCluster, - resolved_product_image: &ResolvedProductImage, - kafka_security: &KafkaTlsSecurity, - rolegroup: &RoleGroupRef, - merged_config: &BrokerConfig, -) -> Result { - Ok(listener::v1alpha1::Listener { - metadata: ObjectMetaBuilder::new() - .name_and_namespace(kafka) - .name(kafka.bootstrap_service_name(rolegroup)) - .ownerreference_from_resource(kafka, None, Some(true)) - .context(ObjectMissingMetadataForOwnerRefSnafu)? - .with_recommended_labels(&build_recommended_labels( - kafka, - KAFKA_CONTROLLER_NAME, - &resolved_product_image.app_version_label_value, - &rolegroup.role, - &rolegroup.role_group, - )) - .context(MetadataBuildSnafu)? - .build(), - spec: listener::v1alpha1::ListenerSpec { - class_name: Some(merged_config.bootstrap_listener_class.clone()), - ports: Some(bootstrap_listener_ports(kafka_security)), - ..listener::v1alpha1::ListenerSpec::default() - }, - status: None, - }) -} - -fn bootstrap_listener_ports( - kafka_security: &KafkaTlsSecurity, -) -> Vec { - vec![if kafka_security.has_kerberos_enabled() { - listener::v1alpha1::ListenerPort { - name: kafka_security.bootstrap_port_name().to_string(), - port: kafka_security.bootstrap_port().into(), - protocol: Some("TCP".to_string()), - } - } else { - listener::v1alpha1::ListenerPort { - name: kafka_security.client_port_name().to_string(), - port: kafka_security.client_port().into(), - protocol: Some("TCP".to_string()), - } - }] -} diff --git a/rust/operator-binary/src/resource/mod.rs b/rust/operator-binary/src/resource/mod.rs deleted file mode 100644 index a79483f8..00000000 --- a/rust/operator-binary/src/resource/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod configmap; -pub mod listener; -pub mod service; -pub mod statefulset; diff --git a/rust/operator-binary/src/resource/service.rs b/rust/operator-binary/src/resource/service.rs deleted file mode 100644 index f430fc8e..00000000 --- a/rust/operator-binary/src/resource/service.rs +++ /dev/null @@ -1,160 +0,0 @@ -use snafu::{ResultExt, Snafu}; -use stackable_operator::{ - builder::meta::ObjectMetaBuilder, - commons::product_image_selection::ResolvedProductImage, - k8s_openapi::api::core::v1::{Service, ServicePort, ServiceSpec}, - kvp::{Annotations, Labels}, - role_utils::RoleGroupRef, -}; - -use crate::{ - controller::KAFKA_CONTROLLER_NAME, - crd::{APP_NAME, METRICS_PORT, METRICS_PORT_NAME, security::KafkaTlsSecurity, v1alpha1}, - utils::build_recommended_labels, -}; - -#[derive(Snafu, Debug)] -pub enum Error { - #[snafu(display("failed to build Metadata"))] - MetadataBuild { - source: stackable_operator::builder::meta::Error, - }, - - #[snafu(display("failed to build Labels"))] - LabelBuild { - source: stackable_operator::kvp::LabelError, - }, - - #[snafu(display("object is missing metadata to build owner reference"))] - ObjectMissingMetadataForOwnerRef { - source: stackable_operator::builder::meta::Error, - }, -} - -/// The rolegroup [`Service`] is a headless service that allows direct access to the instances of a certain rolegroup -/// -/// This is mostly useful for internal communication between peers, or for clients that perform client-side load balancing. -pub fn build_rolegroup_headless_service( - kafka: &v1alpha1::KafkaCluster, - resolved_product_image: &ResolvedProductImage, - rolegroup: &RoleGroupRef, - kafka_security: &KafkaTlsSecurity, -) -> Result { - Ok(Service { - metadata: ObjectMetaBuilder::new() - .name_and_namespace(kafka) - .name(rolegroup.rolegroup_headless_service_name()) - .ownerreference_from_resource(kafka, None, Some(true)) - .context(ObjectMissingMetadataForOwnerRefSnafu)? - .with_recommended_labels(&build_recommended_labels( - kafka, - KAFKA_CONTROLLER_NAME, - &resolved_product_image.app_version_label_value, - &rolegroup.role, - &rolegroup.role_group, - )) - .context(MetadataBuildSnafu)? - .build(), - spec: Some(ServiceSpec { - cluster_ip: Some("None".to_string()), - ports: Some(headless_ports(kafka_security)), - selector: Some( - Labels::role_group_selector( - kafka, - APP_NAME, - &rolegroup.role, - &rolegroup.role_group, - ) - .context(LabelBuildSnafu)? - .into(), - ), - publish_not_ready_addresses: Some(true), - ..ServiceSpec::default() - }), - status: None, - }) -} - -/// The rolegroup metrics [`Service`] is a service that exposes metrics and a prometheus scraping label -pub fn build_rolegroup_metrics_service( - kafka: &v1alpha1::KafkaCluster, - resolved_product_image: &ResolvedProductImage, - rolegroup: &RoleGroupRef, -) -> Result { - let metrics_service = Service { - metadata: ObjectMetaBuilder::new() - .name_and_namespace(kafka) - .name(rolegroup.rolegroup_metrics_service_name()) - .ownerreference_from_resource(kafka, None, Some(true)) - .context(ObjectMissingMetadataForOwnerRefSnafu)? - .with_recommended_labels(&build_recommended_labels( - kafka, - KAFKA_CONTROLLER_NAME, - &resolved_product_image.app_version_label_value, - &rolegroup.role, - &rolegroup.role_group, - )) - .context(MetadataBuildSnafu)? - .with_labels(prometheus_labels()) - .with_annotations(prometheus_annotations()) - .build(), - spec: Some(ServiceSpec { - // Internal communication does not need to be exposed - type_: Some("ClusterIP".to_string()), - cluster_ip: Some("None".to_string()), - ports: Some(metrics_ports()), - selector: Some( - Labels::role_group_selector( - kafka, - APP_NAME, - &rolegroup.role, - &rolegroup.role_group, - ) - .context(LabelBuildSnafu)? - .into(), - ), - publish_not_ready_addresses: Some(true), - ..ServiceSpec::default() - }), - status: None, - }; - Ok(metrics_service) -} - -fn metrics_ports() -> Vec { - vec![ServicePort { - name: Some(METRICS_PORT_NAME.to_string()), - port: METRICS_PORT.into(), - protocol: Some("TCP".to_string()), - ..ServicePort::default() - }] -} - -fn headless_ports(kafka_security: &KafkaTlsSecurity) -> Vec { - vec![ServicePort { - name: Some(kafka_security.client_port_name().into()), - port: kafka_security.client_port().into(), - protocol: Some("TCP".to_string()), - ..ServicePort::default() - }] -} - -/// Common labels for Prometheus -fn prometheus_labels() -> Labels { - Labels::try_from([("prometheus.io/scrape", "true")]).expect("should be a valid label") -} - -/// Common annotations for Prometheus -/// -/// These annotations can be used in a ServiceMonitor. -/// -/// see also -fn prometheus_annotations() -> Annotations { - Annotations::try_from([ - ("prometheus.io/path".to_owned(), "/metrics".to_owned()), - ("prometheus.io/port".to_owned(), METRICS_PORT.to_string()), - ("prometheus.io/scheme".to_owned(), "http".to_owned()), - ("prometheus.io/scrape".to_owned(), "true".to_owned()), - ]) - .expect("should be valid annotations") -} diff --git a/rust/operator-binary/src/resource/statefulset.rs b/rust/operator-binary/src/resource/statefulset.rs deleted file mode 100644 index 5cb262f0..00000000 --- a/rust/operator-binary/src/resource/statefulset.rs +++ /dev/null @@ -1,933 +0,0 @@ -use std::{ - collections::{BTreeMap, HashMap}, - ops::Deref, -}; - -use product_config::types::PropertyNameKind; -use snafu::{OptionExt, ResultExt, Snafu}; -use stackable_operator::{ - builder::{ - meta::ObjectMetaBuilder, - pod::{ - PodBuilder, - container::ContainerBuilder, - resources::ResourceRequirementsBuilder, - security::PodSecurityContextBuilder, - volume::{ListenerOperatorVolumeSourceBuilder, ListenerReference, VolumeBuilder}, - }, - }, - commons::product_image_selection::ResolvedProductImage, - constants::RESTART_CONTROLLER_ENABLED_LABEL, - k8s_openapi::{ - DeepMerge, - api::{ - apps::v1::{StatefulSet, StatefulSetSpec, StatefulSetUpdateStrategy}, - core::v1::{ - ConfigMapKeySelector, ConfigMapVolumeSource, ContainerPort, EnvVar, EnvVarSource, - ExecAction, ObjectFieldSelector, PodSpec, Probe, ServiceAccount, TCPSocketAction, - Volume, - }, - }, - apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString}, - }, - kube::ResourceExt, - kvp::Labels, - product_logging::{ - self, - spec::{ - ConfigMapLogConfig, ContainerLogConfig, ContainerLogConfigChoice, - CustomContainerLogConfig, - }, - }, - role_utils::RoleGroupRef, - utils::cluster_info::KubernetesClusterInfo, -}; - -use crate::{ - config::{ - command::{broker_kafka_container_commands, controller_kafka_container_command}, - node_id_hasher::node_id_hash32_offset, - }, - controller::KAFKA_CONTROLLER_NAME, - crd::{ - self, APP_NAME, KAFKA_HEAP_OPTS, LISTENER_BOOTSTRAP_VOLUME_NAME, - LISTENER_BROKER_VOLUME_NAME, LOG_DIRS_VOLUME_NAME, METRICS_PORT, METRICS_PORT_NAME, - MetadataManager, STACKABLE_CONFIG_DIR, STACKABLE_DATA_DIR, - STACKABLE_LISTENER_BOOTSTRAP_DIR, STACKABLE_LISTENER_BROKER_DIR, - role::{ - AnyConfig, KAFKA_NODE_ID_OFFSET, KafkaRole, broker::BrokerContainer, - controller::ControllerContainer, - }, - security::KafkaTlsSecurity, - v1alpha1, - }, - kerberos::add_kerberos_pod_config, - operations::graceful_shutdown::add_graceful_shutdown_config, - product_logging::{ - BROKER_ID_POD_MAP_DIR, MAX_KAFKA_LOG_FILES_SIZE, STACKABLE_LOG_CONFIG_DIR, - STACKABLE_LOG_DIR, kafka_log_opts, kafka_log_opts_env_var, - }, - utils::build_recommended_labels, -}; - -#[derive(Snafu, Debug)] -pub enum Error { - #[snafu(display("invalid metadata manager"))] - InvalidMetadataManager { source: crate::crd::Error }, - - #[snafu(display("failed to add kerberos config"))] - AddKerberosConfig { source: crate::kerberos::Error }, - - #[snafu(display("failed to add listener volume"))] - AddListenerVolume { - source: stackable_operator::builder::pod::Error, - }, - - #[snafu(display("failed to add Secret Volumes and VolumeMounts"))] - AddVolumesAndVolumeMounts { source: crate::crd::security::Error }, - - #[snafu(display("failed to add needed volumeMount"))] - AddVolumeMount { - source: stackable_operator::builder::pod::container::Error, - }, - - #[snafu(display("failed to add needed volume"))] - AddVolume { - source: stackable_operator::builder::pod::Error, - }, - - #[snafu(display("failed to build bootstrap listener pvc"))] - BuildBootstrapListenerPvc { - source: stackable_operator::builder::pod::volume::ListenerOperatorVolumeSourceBuilderError, - }, - - #[snafu(display("failed to build pod descriptors"))] - BuildPodDescriptors { source: crate::crd::Error }, - - #[snafu(display("failed to configure logging"))] - ConfigureLogging { - source: stackable_operator::product_logging::framework::LoggingError, - }, - - #[snafu(display("failed to construct JVM arguments"))] - ConstructJvmArguments { source: crate::crd::role::Error }, - - #[snafu(display("failed to configure graceful shutdown"))] - GracefulShutdown { - source: crate::operations::graceful_shutdown::Error, - }, - - #[snafu(display("invalid Container name [{name}]"))] - InvalidContainerName { - name: String, - source: stackable_operator::builder::pod::container::Error, - }, - - #[snafu(display("invalid kafka listeners"))] - InvalidKafkaListeners { - source: crate::crd::listener::KafkaListenerError, - }, - - #[snafu(display("failed to build Labels"))] - LabelBuild { - source: stackable_operator::kvp::LabelError, - }, - - #[snafu(display("failed to merge pod overrides"))] - MergePodOverrides { source: crd::role::Error }, - - #[snafu(display("failed to build Metadata"))] - MetadataBuild { - source: stackable_operator::builder::meta::Error, - }, - - #[snafu(display("missing secret lifetime"))] - MissingSecretLifetime, - - #[snafu(display("object is missing metadata to build owner reference"))] - ObjectMissingMetadataForOwnerRef { - source: stackable_operator::builder::meta::Error, - }, - - #[snafu(display("failed to retrieve rolegroup replicas"))] - RoleGroupReplicas { source: crd::role::Error }, - - #[snafu(display( - "cluster does not define 'metadata.name' which is required for the Kafka cluster id" - ))] - ClusterIdMissing, - - #[snafu(display("vector agent is enabled but vector aggregator ConfigMap is missing"))] - VectorAggregatorConfigMapMissing, -} - -/// The broker rolegroup [`StatefulSet`] runs the rolegroup, as configured by the administrator. -/// -/// The [`Pod`](`stackable_operator::k8s_openapi::api::core::v1::Pod`)s are accessible through the corresponding -/// [`Service`](`stackable_operator::k8s_openapi::api::core::v1::Service`) from [`build_rolegroup_service`](`crate::resource::service::build_rolegroup_headless_service`). -#[allow(clippy::too_many_arguments)] -pub fn build_broker_rolegroup_statefulset( - kafka: &v1alpha1::KafkaCluster, - kafka_role: &KafkaRole, - resolved_product_image: &ResolvedProductImage, - rolegroup_ref: &RoleGroupRef, - broker_config: &HashMap>, - kafka_security: &KafkaTlsSecurity, - merged_config: &AnyConfig, - service_account: &ServiceAccount, - cluster_info: &KubernetesClusterInfo, -) -> Result { - let recommended_object_labels = build_recommended_labels( - kafka, - KAFKA_CONTROLLER_NAME, - &resolved_product_image.app_version_label_value, - &rolegroup_ref.role, - &rolegroup_ref.role_group, - ); - let recommended_labels = - Labels::recommended(&recommended_object_labels).context(LabelBuildSnafu)?; - // Used for PVC templates that cannot be modified once they are deployed - let unversioned_recommended_labels = Labels::recommended(&build_recommended_labels( - kafka, - KAFKA_CONTROLLER_NAME, - // A version value is required, and we do want to use the "recommended" format for the other desired labels - "none", - &rolegroup_ref.role, - &rolegroup_ref.role_group, - )) - .context(LabelBuildSnafu)?; - - let kcat_prober_container_name = BrokerContainer::KcatProber.to_string(); - let mut cb_kcat_prober = - ContainerBuilder::new(&kcat_prober_container_name).context(InvalidContainerNameSnafu { - name: kcat_prober_container_name.clone(), - })?; - - let kafka_container_name = BrokerContainer::Kafka.to_string(); - let mut cb_kafka = - ContainerBuilder::new(&kafka_container_name).context(InvalidContainerNameSnafu { - name: kafka_container_name.clone(), - })?; - - let mut pod_builder = PodBuilder::new(); - - // Add TLS related volumes and volume mounts - let requested_secret_lifetime = merged_config - .deref() - .requested_secret_lifetime - .context(MissingSecretLifetimeSnafu)?; - kafka_security - .add_broker_volume_and_volume_mounts( - &mut pod_builder, - &mut cb_kcat_prober, - &mut cb_kafka, - &requested_secret_lifetime, - ) - .context(AddVolumesAndVolumeMountsSnafu)?; - - let mut pvcs = merged_config.resources().storage.build_pvcs(); - - // bootstrap listener should be persistent, - // main broker listener is an ephemeral PVC instead - pvcs.push( - ListenerOperatorVolumeSourceBuilder::new( - &ListenerReference::ListenerName(kafka.bootstrap_service_name(rolegroup_ref)), - &unversioned_recommended_labels, - ) - .build_pvc(LISTENER_BOOTSTRAP_VOLUME_NAME) - .context(BuildBootstrapListenerPvcSnafu)?, - ); - - if kafka_security.has_kerberos_enabled() { - add_kerberos_pod_config( - kafka_security, - kafka_role, - &mut cb_kcat_prober, - &mut cb_kafka, - &mut pod_builder, - ) - .context(AddKerberosConfigSnafu)?; - } - - let mut env = broker_config - .get(&PropertyNameKind::Env) - .into_iter() - .flatten() - .map(|(k, v)| EnvVar { - name: k.clone(), - value: Some(v.clone()), - ..EnvVar::default() - }) - .collect::>(); - - if let Some(zookeeper_config_map_name) = &kafka.spec.cluster_config.zookeeper_config_map_name { - env.push(EnvVar { - name: "ZOOKEEPER".to_string(), - value_from: Some(EnvVarSource { - config_map_key_ref: Some(ConfigMapKeySelector { - name: zookeeper_config_map_name.to_string(), - key: "ZOOKEEPER".to_string(), - ..ConfigMapKeySelector::default() - }), - ..EnvVarSource::default() - }), - ..EnvVar::default() - }) - }; - - env.push(EnvVar { - name: "POD_NAME".to_string(), - value_from: Some(EnvVarSource { - field_ref: Some(ObjectFieldSelector { - api_version: Some("v1".to_string()), - field_path: "metadata.name".to_string(), - }), - ..EnvVarSource::default() - }), - ..EnvVar::default() - }); - - let metadata_manager = kafka - .effective_metadata_manager() - .context(InvalidMetadataManagerSnafu)?; - - cb_kafka - .image_from_product_image(resolved_product_image) - .command(vec![ - "/bin/bash".to_string(), - "-x".to_string(), - "-euo".to_string(), - "pipefail".to_string(), - "-c".to_string(), - ]) - .args(vec![broker_kafka_container_commands( - metadata_manager == MetadataManager::KRaft, - // we need controller pods - kafka - .pod_descriptors( - Some(&KafkaRole::Controller), - cluster_info, - kafka_security.client_port(), - ) - .context(BuildPodDescriptorsSnafu)?, - kafka_security, - &resolved_product_image.product_version, - )]) - .add_env_var( - "EXTRA_ARGS", - kafka_role - .construct_non_heap_jvm_args(merged_config, kafka, &rolegroup_ref.role_group) - .context(ConstructJvmArgumentsSnafu)?, - ) - .add_env_var( - KAFKA_HEAP_OPTS, - kafka_role - .construct_heap_jvm_args(merged_config, kafka, &rolegroup_ref.role_group) - .context(ConstructJvmArgumentsSnafu)?, - ) - .add_env_var( - kafka_log_opts_env_var(), - kafka_log_opts(&resolved_product_image.product_version), - ) - // Needed for the `containerdebug` process to log it's tracing information to. - .add_env_var( - "CONTAINERDEBUG_LOG_DIRECTORY", - format!("{STACKABLE_LOG_DIR}/containerdebug"), - ) - .add_env_var( - KAFKA_NODE_ID_OFFSET, - node_id_hash32_offset(rolegroup_ref).to_string(), - ) - .add_env_var( - "KAFKA_CLIENT_PORT".to_string(), - kafka_security.client_port().to_string(), - ) - .add_env_vars(env) - .add_container_ports(container_ports(kafka_security)) - .add_volume_mount(LOG_DIRS_VOLUME_NAME, STACKABLE_DATA_DIR) - .context(AddVolumeMountSnafu)? - .add_volume_mount("config", STACKABLE_CONFIG_DIR) - .context(AddVolumeMountSnafu)? - .add_volume_mount( - LISTENER_BOOTSTRAP_VOLUME_NAME, - STACKABLE_LISTENER_BOOTSTRAP_DIR, - ) - .context(AddVolumeMountSnafu)? - .add_volume_mount(LISTENER_BROKER_VOLUME_NAME, STACKABLE_LISTENER_BROKER_DIR) - .context(AddVolumeMountSnafu)? - .add_volume_mount("log-config", STACKABLE_LOG_CONFIG_DIR) - .context(AddVolumeMountSnafu)? - .add_volume_mount("log", STACKABLE_LOG_DIR) - .context(AddVolumeMountSnafu)? - .resources(merged_config.resources().clone().into()); - - // Use kcat sidecar for probing container status rather than the official Kafka tools, since they incur a lot of - // unacceptable perf overhead - cb_kcat_prober - .image_from_product_image(resolved_product_image) - .command(vec!["sleep".to_string(), "infinity".to_string()]) - .add_env_vars(vec![EnvVar { - name: "POD_NAME".to_string(), - value_from: Some(EnvVarSource { - field_ref: Some(ObjectFieldSelector { - api_version: Some("v1".to_string()), - field_path: "metadata.name".to_string(), - }), - ..EnvVarSource::default() - }), - ..EnvVar::default() - }]) - .resources( - ResourceRequirementsBuilder::new() - .with_cpu_request("100m") - .with_cpu_limit("200m") - .with_memory_request("128Mi") - .with_memory_limit("128Mi") - .build(), - ) - .add_volume_mount( - LISTENER_BOOTSTRAP_VOLUME_NAME, - STACKABLE_LISTENER_BOOTSTRAP_DIR, - ) - .context(AddVolumeMountSnafu)? - .add_volume_mount(LISTENER_BROKER_VOLUME_NAME, STACKABLE_LISTENER_BROKER_DIR) - .context(AddVolumeMountSnafu)? - // Only allow the global load balancing service to send traffic to pods that are members of the quorum - // This also acts as a hint to the StatefulSet controller to wait for each pod to enter quorum before taking down the next - .readiness_probe(Probe { - exec: Some(ExecAction { - // If the broker is able to get its fellow cluster members then it has at least completed basic registration at some point - command: Some(kafka_security.kcat_prober_container_commands()), - }), - timeout_seconds: Some(5), - period_seconds: Some(2), - ..Probe::default() - }); - - if let ContainerLogConfig { - choice: - Some(ContainerLogConfigChoice::Custom(CustomContainerLogConfig { - custom: ConfigMapLogConfig { config_map }, - })), - } = &*merged_config.kafka_logging() - { - pod_builder - .add_volume( - VolumeBuilder::new("log-config") - .with_config_map(config_map) - .build(), - ) - .context(AddVolumeSnafu)?; - } else { - pod_builder - .add_volume( - VolumeBuilder::new("log-config") - .with_config_map(rolegroup_ref.object_name()) - .build(), - ) - .context(AddVolumeSnafu)?; - } - - let metadata = ObjectMetaBuilder::new() - .with_recommended_labels(&recommended_object_labels) - .context(MetadataBuildSnafu)? - .build(); - - if let Some(listener_class) = merged_config.listener_class() { - pod_builder - .add_listener_volume_by_listener_class( - LISTENER_BROKER_VOLUME_NAME, - listener_class, - &recommended_labels, - ) - .context(AddListenerVolumeSnafu)?; - } - - if let Some(broker_id_config_map_name) = - &kafka.spec.cluster_config.broker_id_pod_config_map_name - { - pod_builder - .add_volume( - VolumeBuilder::new("broker-id-pod-map-dir") - .with_config_map(broker_id_config_map_name) - .build(), - ) - .context(AddVolumeSnafu)?; - cb_kafka - .add_volume_mount("broker-id-pod-map-dir", BROKER_ID_POD_MAP_DIR) - .context(AddVolumeMountSnafu)?; - } - - pod_builder - .metadata(metadata) - .image_pull_secrets_from_product_image(resolved_product_image) - .add_container(cb_kafka.build()) - .add_container(cb_kcat_prober.build()) - .affinity(&merged_config.affinity) - .add_volume(Volume { - name: "config".to_string(), - config_map: Some(ConfigMapVolumeSource { - name: rolegroup_ref.object_name(), - ..ConfigMapVolumeSource::default() - }), - ..Volume::default() - }) - .context(AddVolumeSnafu)? - // bootstrap volume is a persistent volume template instead, to keep addresses persistent - .add_empty_dir_volume( - "log", - Some(product_logging::framework::calculate_log_volume_size_limit( - &[MAX_KAFKA_LOG_FILES_SIZE], - )), - ) - .context(AddVolumeSnafu)? - .service_account_name(service_account.name_any()) - .security_context(PodSecurityContextBuilder::new().fs_group(1000).build()); - - // Add vector container after kafka container to keep the defaulting into kafka container - if merged_config.vector_logging_enabled() { - match &kafka.spec.cluster_config.vector_aggregator_config_map_name { - Some(vector_aggregator_config_map_name) => { - pod_builder.add_container( - product_logging::framework::vector_container( - resolved_product_image, - "config", - "log", - Some(&*merged_config.vector_logging()), - ResourceRequirementsBuilder::new() - .with_cpu_request("250m") - .with_cpu_limit("500m") - .with_memory_request("128Mi") - .with_memory_limit("128Mi") - .build(), - vector_aggregator_config_map_name, - ) - .context(ConfigureLoggingSnafu)?, - ); - } - None => { - VectorAggregatorConfigMapMissingSnafu.fail()?; - } - } - } - - add_graceful_shutdown_config(merged_config, &mut pod_builder).context(GracefulShutdownSnafu)?; - - let mut pod_template = pod_builder.build_template(); - - let pod_template_spec = pod_template.spec.get_or_insert_with(PodSpec::default); - // Don't run kcat pod as PID 1, to ensure that default signal handlers apply - pod_template_spec.share_process_namespace = Some(true); - - pod_template.merge_from( - kafka_role - .role_pod_overrides(kafka) - .context(MergePodOverridesSnafu)?, - ); - pod_template.merge_from( - kafka_role - .role_group_pod_overrides(kafka, &rolegroup_ref.role_group) - .context(MergePodOverridesSnafu)?, - ); - - Ok(StatefulSet { - metadata: ObjectMetaBuilder::new() - .name_and_namespace(kafka) - .name(rolegroup_ref.object_name()) - .ownerreference_from_resource(kafka, None, Some(true)) - .context(ObjectMissingMetadataForOwnerRefSnafu)? - .with_recommended_labels(&build_recommended_labels( - kafka, - KAFKA_CONTROLLER_NAME, - &resolved_product_image.app_version_label_value, - &rolegroup_ref.role, - &rolegroup_ref.role_group, - )) - .context(MetadataBuildSnafu)? - .with_label(RESTART_CONTROLLER_ENABLED_LABEL.to_owned()) - .build(), - spec: Some(StatefulSetSpec { - pod_management_policy: Some("Parallel".to_string()), - replicas: kafka_role - .replicas(kafka, &rolegroup_ref.role_group) - .context(RoleGroupReplicasSnafu)? - .map(i32::from), - selector: LabelSelector { - match_labels: Some( - Labels::role_group_selector( - kafka, - APP_NAME, - &rolegroup_ref.role, - &rolegroup_ref.role_group, - ) - .context(LabelBuildSnafu)? - .into(), - ), - ..LabelSelector::default() - }, - service_name: Some(rolegroup_ref.rolegroup_headless_service_name()), - template: pod_template, - volume_claim_templates: Some(pvcs), - ..StatefulSetSpec::default() - }), - status: None, - }) -} - -/// The controller rolegroup [`StatefulSet`] runs the rolegroup, as configured by the administrator. -#[allow(clippy::too_many_arguments)] -pub fn build_controller_rolegroup_statefulset( - kafka: &v1alpha1::KafkaCluster, - kafka_role: &KafkaRole, - resolved_product_image: &ResolvedProductImage, - rolegroup_ref: &RoleGroupRef, - controller_config: &HashMap>, - kafka_security: &KafkaTlsSecurity, - merged_config: &AnyConfig, - service_account: &ServiceAccount, - cluster_info: &KubernetesClusterInfo, -) -> Result { - let recommended_object_labels = build_recommended_labels( - kafka, - KAFKA_CONTROLLER_NAME, - &resolved_product_image.app_version_label_value, - &rolegroup_ref.role, - &rolegroup_ref.role_group, - ); - - let kafka_container_name = ControllerContainer::Kafka.to_string(); - let mut cb_kafka = - ContainerBuilder::new(&kafka_container_name).context(InvalidContainerNameSnafu { - name: kafka_container_name.clone(), - })?; - - let mut pod_builder = PodBuilder::new(); - - let mut env = controller_config - .get(&PropertyNameKind::Env) - .into_iter() - .flatten() - .map(|(k, v)| EnvVar { - name: k.clone(), - value: Some(v.clone()), - ..EnvVar::default() - }) - .collect::>(); - - env.push(EnvVar { - name: "NAMESPACE".to_string(), - value_from: Some(EnvVarSource { - field_ref: Some(ObjectFieldSelector { - api_version: Some("v1".to_string()), - field_path: "metadata.namespace".to_string(), - }), - ..EnvVarSource::default() - }), - ..EnvVar::default() - }); - - env.push(EnvVar { - name: "POD_NAME".to_string(), - value_from: Some(EnvVarSource { - field_ref: Some(ObjectFieldSelector { - api_version: Some("v1".to_string()), - field_path: "metadata.name".to_string(), - }), - ..EnvVarSource::default() - }), - ..EnvVar::default() - }); - - env.push(EnvVar { - name: "ROLEGROUP_HEADLESS_SERVICE_NAME".to_string(), - value: Some(rolegroup_ref.rolegroup_headless_service_name()), - ..EnvVar::default() - }); - - env.push(EnvVar { - name: "CLUSTER_DOMAIN".to_string(), - value: Some(cluster_info.cluster_domain.to_string()), - ..EnvVar::default() - }); - - env.push(EnvVar { - name: "KAFKA_CLIENT_PORT".to_string(), - value: Some(kafka_security.client_port().to_string()), - ..EnvVar::default() - }); - - // Controllers need the ZooKeeper connection string for migration - if let Some(zookeeper_config_map_name) = &kafka.spec.cluster_config.zookeeper_config_map_name { - env.push(EnvVar { - name: "ZOOKEEPER".to_string(), - value_from: Some(EnvVarSource { - config_map_key_ref: Some(ConfigMapKeySelector { - name: zookeeper_config_map_name.to_string(), - key: "ZOOKEEPER".to_string(), - ..ConfigMapKeySelector::default() - }), - ..EnvVarSource::default() - }), - ..EnvVar::default() - }) - }; - - cb_kafka - .image_from_product_image(resolved_product_image) - .command(vec![ - "/bin/bash".to_string(), - "-x".to_string(), - "-euo".to_string(), - "pipefail".to_string(), - "-c".to_string(), - ]) - .args(vec![controller_kafka_container_command( - kafka - .pod_descriptors(Some(kafka_role), cluster_info, kafka_security.client_port()) - .context(BuildPodDescriptorsSnafu)?, - &resolved_product_image.product_version, - )]) - .add_env_var("PRE_STOP_CONTROLLER_SLEEP_SECONDS", "10") - .add_env_var( - "EXTRA_ARGS", - kafka_role - .construct_non_heap_jvm_args(merged_config, kafka, &rolegroup_ref.role_group) - .context(ConstructJvmArgumentsSnafu)?, - ) - .add_env_var( - KAFKA_HEAP_OPTS, - kafka_role - .construct_heap_jvm_args(merged_config, kafka, &rolegroup_ref.role_group) - .context(ConstructJvmArgumentsSnafu)?, - ) - .add_env_var( - kafka_log_opts_env_var(), - kafka_log_opts(&resolved_product_image.product_version), - ) - // Needed for the `containerdebug` process to log it's tracing information to. - .add_env_var( - "CONTAINERDEBUG_LOG_DIRECTORY", - format!("{STACKABLE_LOG_DIR}/containerdebug"), - ) - .add_env_var( - KAFKA_NODE_ID_OFFSET, - node_id_hash32_offset(rolegroup_ref).to_string(), - ) - .add_env_vars(env) - .add_container_ports(container_ports(kafka_security)) - .add_volume_mount(LOG_DIRS_VOLUME_NAME, STACKABLE_DATA_DIR) - .context(AddVolumeMountSnafu)? - .add_volume_mount("config", STACKABLE_CONFIG_DIR) - .context(AddVolumeMountSnafu)? - .add_volume_mount("log-config", STACKABLE_LOG_CONFIG_DIR) - .context(AddVolumeMountSnafu)? - .add_volume_mount("log", STACKABLE_LOG_DIR) - .context(AddVolumeMountSnafu)? - .resources(merged_config.resources().clone().into()) - // TODO: improve probes - .liveness_probe(Probe { - tcp_socket: Some(TCPSocketAction { - port: IntOrString::Int(kafka_security.client_port().into()), - ..Default::default() - }), - timeout_seconds: Some(10), - period_seconds: Some(10), - failure_threshold: Some(6), - ..Probe::default() - }) - .readiness_probe(Probe { - tcp_socket: Some(TCPSocketAction { - port: IntOrString::Int(kafka_security.client_port().into()), - ..Default::default() - }), - timeout_seconds: Some(10), - period_seconds: Some(10), - failure_threshold: Some(6), - ..Probe::default() - }); - - if let ContainerLogConfig { - choice: - Some(ContainerLogConfigChoice::Custom(CustomContainerLogConfig { - custom: ConfigMapLogConfig { config_map }, - })), - } = &*merged_config.kafka_logging() - { - pod_builder - .add_volume( - VolumeBuilder::new("log-config") - .with_config_map(config_map) - .build(), - ) - .context(AddVolumeSnafu)?; - } else { - pod_builder - .add_volume( - VolumeBuilder::new("log-config") - .with_config_map(rolegroup_ref.object_name()) - .build(), - ) - .context(AddVolumeSnafu)?; - } - - let metadata = ObjectMetaBuilder::new() - .with_recommended_labels(&recommended_object_labels) - .context(MetadataBuildSnafu)? - .build(); - - // Add TLS related volumes and volume mounts - let requested_secret_lifetime = merged_config - .deref() - .requested_secret_lifetime - .context(MissingSecretLifetimeSnafu)?; - kafka_security - .add_controller_volume_and_volume_mounts( - &mut pod_builder, - &mut cb_kafka, - &requested_secret_lifetime, - ) - .context(AddVolumesAndVolumeMountsSnafu)?; - - let kafka_container = cb_kafka.build(); - - pod_builder - .metadata(metadata) - .image_pull_secrets_from_product_image(resolved_product_image) - .add_container(kafka_container) - .affinity(&merged_config.affinity) - .add_volume(Volume { - name: "config".to_string(), - config_map: Some(ConfigMapVolumeSource { - name: rolegroup_ref.object_name(), - ..ConfigMapVolumeSource::default() - }), - ..Volume::default() - }) - .context(AddVolumeSnafu)? - // bootstrap volume is a persistent volume template instead, to keep addresses persistent - .add_empty_dir_volume( - "log", - Some(product_logging::framework::calculate_log_volume_size_limit( - &[MAX_KAFKA_LOG_FILES_SIZE], - )), - ) - .context(AddVolumeSnafu)? - .service_account_name(service_account.name_any()) - .security_context(PodSecurityContextBuilder::new().fs_group(1000).build()); - - // Add vector container after kafka container to keep the defaulting into kafka container - if merged_config.vector_logging_enabled() { - match &kafka.spec.cluster_config.vector_aggregator_config_map_name { - Some(vector_aggregator_config_map_name) => { - pod_builder.add_container( - product_logging::framework::vector_container( - resolved_product_image, - "config", - "log", - Some(&*merged_config.vector_logging()), - ResourceRequirementsBuilder::new() - .with_cpu_request("250m") - .with_cpu_limit("500m") - .with_memory_request("128Mi") - .with_memory_limit("128Mi") - .build(), - vector_aggregator_config_map_name, - ) - .context(ConfigureLoggingSnafu)?, - ); - } - None => { - VectorAggregatorConfigMapMissingSnafu.fail()?; - } - } - } - - add_graceful_shutdown_config(merged_config, &mut pod_builder).context(GracefulShutdownSnafu)?; - - let mut pod_template = pod_builder.build_template(); - - pod_template.merge_from( - kafka_role - .role_pod_overrides(kafka) - .context(MergePodOverridesSnafu)?, - ); - pod_template.merge_from( - kafka_role - .role_group_pod_overrides(kafka, &rolegroup_ref.role_group) - .context(MergePodOverridesSnafu)?, - ); - - Ok(StatefulSet { - metadata: ObjectMetaBuilder::new() - .name_and_namespace(kafka) - .name(rolegroup_ref.object_name()) - .ownerreference_from_resource(kafka, None, Some(true)) - .context(ObjectMissingMetadataForOwnerRefSnafu)? - .with_recommended_labels(&build_recommended_labels( - kafka, - KAFKA_CONTROLLER_NAME, - &resolved_product_image.app_version_label_value, - &rolegroup_ref.role, - &rolegroup_ref.role_group, - )) - .context(MetadataBuildSnafu)? - .with_label(RESTART_CONTROLLER_ENABLED_LABEL.to_owned()) - .build(), - spec: Some(StatefulSetSpec { - pod_management_policy: Some("Parallel".to_string()), - update_strategy: Some(StatefulSetUpdateStrategy { - type_: Some("RollingUpdate".to_string()), - ..StatefulSetUpdateStrategy::default() - }), - replicas: kafka_role - .replicas(kafka, &rolegroup_ref.role_group) - .context(RoleGroupReplicasSnafu)? - .map(i32::from), - selector: LabelSelector { - match_labels: Some( - Labels::role_group_selector( - kafka, - APP_NAME, - &rolegroup_ref.role, - &rolegroup_ref.role_group, - ) - .context(LabelBuildSnafu)? - .into(), - ), - ..LabelSelector::default() - }, - service_name: Some(rolegroup_ref.rolegroup_headless_service_name()), - template: pod_template, - volume_claim_templates: Some(merged_config.resources().storage.build_pvcs()), - ..StatefulSetSpec::default() - }), - status: None, - }) -} - -/// We only expose client HTTP / HTTPS and Metrics ports. -fn container_ports(kafka_security: &KafkaTlsSecurity) -> Vec { - let mut ports = vec![ - ContainerPort { - name: Some(METRICS_PORT_NAME.to_string()), - container_port: METRICS_PORT.into(), - protocol: Some("TCP".to_string()), - ..ContainerPort::default() - }, - ContainerPort { - name: Some(kafka_security.client_port_name().to_string()), - container_port: kafka_security.client_port().into(), - protocol: Some("TCP".to_string()), - ..ContainerPort::default() - }, - ]; - if kafka_security.has_kerberos_enabled() { - ports.push(ContainerPort { - name: Some(kafka_security.bootstrap_port_name().to_string()), - container_port: kafka_security.bootstrap_port().into(), - protocol: Some("TCP".to_string()), - ..ContainerPort::default() - }); - } - ports -} diff --git a/rust/operator-binary/src/utils.rs b/rust/operator-binary/src/utils.rs deleted file mode 100644 index 7abbafff..00000000 --- a/rust/operator-binary/src/utils.rs +++ /dev/null @@ -1,22 +0,0 @@ -use stackable_operator::kvp::ObjectLabels; - -use crate::crd::{APP_NAME, OPERATOR_NAME, v1alpha1}; - -/// Build recommended values for labels -pub fn build_recommended_labels<'a>( - owner: &'a v1alpha1::KafkaCluster, - controller_name: &'a str, - app_version: &'a str, - role: &'a str, - role_group: &'a str, -) -> ObjectLabels<'a, v1alpha1::KafkaCluster> { - ObjectLabels { - owner, - app_name: APP_NAME, - app_version, - operator_name: OPERATOR_NAME, - controller_name, - role, - role_group, - } -} diff --git a/tests/templates/kuttl/smoke/33-assert.yaml.j2 b/tests/templates/kuttl/smoke/33-assert.yaml.j2 index 1ed5fe65..70678faa 100644 --- a/tests/templates/kuttl/smoke/33-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/33-assert.yaml.j2 @@ -74,6 +74,32 @@ spec: memory: 128Mi {% if vector_enabled %} - name: vector + env: + - name: CLUSTER_NAME + value: test-kafka + - name: DATA_DIR + value: /stackable/log/_vector-state + - name: LOG_DIR + value: /stackable/log + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: ROLE_GROUP_NAME + value: default + - name: ROLE_NAME + value: broker + - name: VECTOR_AGGREGATOR_ADDRESS + valueFrom: + configMapKeyRef: + key: ADDRESS + name: vector-aggregator-discovery + - name: VECTOR_CONFIG_YAML + value: /stackable/config/vector.yaml + - name: VECTOR_FILE_LOG_LEVEL + value: info + - name: VECTOR_LOG + value: info resources: limits: cpu: 500m diff --git a/tests/templates/kuttl/smoke/34-assert.yaml.j2 b/tests/templates/kuttl/smoke/34-assert.yaml.j2 index 299f2a81..e6164a7e 100644 --- a/tests/templates/kuttl/smoke/34-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/34-assert.yaml.j2 @@ -100,7 +100,8 @@ commands: networkaddress.cache.ttl=30 {% if lookup('env', 'VECTOR_AGGREGATOR') %} vector.yaml: | - data_dir: /stackable/log/_vector-state + --- + data_dir: ${DATA_DIR} log_schema: host_key: pod @@ -109,20 +110,10 @@ commands: vector: type: internal_logs - files_stdout: - type: file - include: - - /stackable/log/*/*.stdout.log - - files_stderr: - type: file - include: - - /stackable/log/*/*.stderr.log - files_log4j: type: file include: - - /stackable/log/*/*.log4j.xml + - ${LOG_DIR}/*/*.log4j.xml line_delimiter: "\r\n" multiline: mode: halt_before @@ -133,194 +124,10 @@ commands: files_log4j2: type: file include: - - /stackable/log/*/*.log4j2.xml + - ${LOG_DIR}/*/*.log4j2.xml line_delimiter: "\r\n" - files_py: - type: file - include: - - /stackable/log/*/*.py.json - - files_airlift: - type: file - include: - - /stackable/log/*/*.airlift.json - - files_tracing_rs: - type: file - include: - - /stackable/log/*/*.tracing-rs.json - - files_opa_json: - type: file - include: - - /stackable/log/opa/current - - /stackable/log/opa/test - transforms: - processed_files_tracing_rs: - inputs: - - files_tracing_rs - type: remap - source: | - raw_message = string!(.message) - - .timestamp = now() - .logger = "" - .level = "INFO" - .message = "" - .errors = [] - - parsed_event, err = parse_json(raw_message) - if err != null { - error = "JSON not parsable: " + err - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else if !is_object(parsed_event) { - error = "Parsed event is not a JSON object." - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else { - event = object!(parsed_event) - - timestamp_string, err = string(event.timestamp) - if err == null { - parsed_timestamp, err = parse_timestamp(timestamp_string, "%+") - if err == null { - .timestamp = parsed_timestamp - } else { - .errors = push(.errors, "Timestamp not parsable, trying current time instead: " + err) - } - } - - .logger, err = string(event.target) - if err != null || is_empty(.logger) { - .errors = push(.errors, "Logger/target not found.") - } - - level, err = string(event.level) - if err != null { - .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") - } else if !includes(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], upcase(level)) { - .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") - } else { - .level = upcase(level) - } - - fields, err = object(event.fields) - if err != null { - .errors = push(.errors, "Fields are not an object.") - } - - .message, err = string(fields.message) - if err != null || is_empty(.message) { - .errors = push(.errors, "Message not found.") - } - - del(fields.message) - - other_fields = encode_key_value(fields, field_delimiter: "\n") - .message = join!(compact([.message, other_fields]), "\n\n") - } - - processed_files_opa_json: - inputs: - - files_opa_json - type: remap - source: | - raw_message = string!(.message) - - .timestamp = now() - .logger = "" - .level = "INFO" - .message = "" - .errors = [] - - parsed_event, err = parse_json(raw_message) - if err != null { - error = "JSON not parsable: " + err - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else if !is_object(parsed_event) { - error = "Parsed event is not a JSON object." - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else { - event = object!(parsed_event) - - property_timestamp_valid = false - timestamp_string, err = string(event.timestamp) - if err == null { - parsed_timestamp, err = parse_timestamp(timestamp_string, "%Y-%m-%dT%H:%M:%S.%fZ") - if err == null { - .timestamp = parsed_timestamp - property_timestamp_valid = true - } else { - .errors = push(.errors, "Timestamp not parsable, trying property time instead: " + err) - } - } - if !property_timestamp_valid { - time_string, err = string(event.time) - if err == null { - parsed_timestamp, err = parse_timestamp(time_string, "%Y-%m-%dT%H:%M:%SZ") - if err == null { - .timestamp = parsed_timestamp - } else { - .errors = push(.errors, "Time not parsable, using current time instead: " + err) - } - } else { - .errors = push(.errors, "Time not found, using current time instead.") - } - } - - .logger, err = string(event.logger) - if err != null || is_empty(.logger) { - .errors = push(.errors, "Logger not found.") - } - - level, err = string(event.level) - if err != null { - .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") - } else if !includes(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], upcase(level)) { - .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") - } else { - .level = upcase(level) - } - - .message, err = string(event.msg) - if err != null || is_empty(.message) { - .errors = push(.errors, "Message not found.") - } - - del(event.time) - del(event.timestamp) - del(event.level) - del(event.msg) - - other_fields = encode_key_value(event, field_delimiter: "\n") - .message = join!(compact([.message, other_fields]), "\n\n") - } - - processed_files_stdout: - inputs: - - files_stdout - type: remap - source: | - .logger = "ROOT" - .level = "INFO" - - processed_files_stderr: - inputs: - - files_stderr - type: remap - source: | - .logger = "ROOT" - .level = "ERROR" - processed_files_log4j: inputs: - files_log4j @@ -521,136 +328,6 @@ commands: } } - processed_files_py: - inputs: - - files_py - type: remap - source: | - raw_message = string!(.message) - - .timestamp = now() - .logger = "" - .level = "INFO" - .message = "" - .errors = [] - - parsed_event, err = parse_json(raw_message) - if err != null { - error = "JSON not parsable: " + err - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else if !is_object(parsed_event) { - error = "Parsed event is not a JSON object." - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else { - event = object!(parsed_event) - - asctime, err = string(event.asctime) - if err == null { - parsed_timestamp, err = parse_timestamp(asctime, "%F %T,%3f") - if err == null { - .timestamp = parsed_timestamp - } else { - .errors = push(.errors, "Timestamp not parsable, using current time instead: "+ err) - } - } else { - .errors = push(.errors, "Timestamp not found, using current time instead.") - } - - .logger, err = string(event.name) - if err != null || is_empty(.logger) { - .errors = push(.errors, "Logger not found.") - } - - level, err = string(event.levelname) - if err != null { - .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") - } else if level == "DEBUG" { - .level = "DEBUG" - } else if level == "INFO" { - .level = "INFO" - } else if level == "WARNING" { - .level = "WARN" - } else if level == "ERROR" { - .level = "ERROR" - } else if level == "CRITICAL" { - .level = "FATAL" - } else { - .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") - } - - .message, err = string(event.message) - if err != null || is_empty(.message) { - .errors = push(.errors, "Message not found.") - } - } - - processed_files_airlift: - inputs: - - files_airlift - type: remap - source: | - raw_message = string!(.message) - - .timestamp = now() - .logger = "" - .level = "INFO" - .message = "" - .errors = [] - - parsed_event, err = parse_json(raw_message) - if err != null { - error = "JSON not parsable: " + err - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else if !is_object(parsed_event) { - error = "Parsed event is not a JSON object." - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else { - event = object!(parsed_event) - - timestamp_string, err = string(event.timestamp) - if err == null { - parsed_timestamp, err = parse_timestamp(timestamp_string, "%Y-%m-%dT%H:%M:%S.%fZ") - if err == null { - .timestamp = parsed_timestamp - } else { - .errors = push(.errors, "Timestamp not parsable, using current time instead: " + err) - } - } else { - .errors = push(.errors, "Timestamp not found, using current time instead.") - } - - .logger, err = string(event.logger) - if err != null || is_empty(.logger) { - .errors = push(.errors, "Logger not found.") - } - - level, err = string(event.level) - if err != null { - .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") - } else if !includes(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], level) { - .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") - } else { - .level = level - } - - .thread = string(parsed_event.thread) ?? null - - .message, err = string(event.message) - if err != null || is_empty(.message) { - .errors = push(.errors, "Message not found.") - } - stacktrace = string(event.stackTrace) ?? "" - .message = join!(compact([.message, stacktrace]), "\n\n") - } - extended_logs_files: inputs: - processed_files_* @@ -660,7 +337,7 @@ commands: if .errors == [] { del(.errors) } - . |= parse_regex!(.file, r'^/stackable/log/(?P.*?)/(?P.*?)$') + . |= parse_regex!(.file, r'^${LOG_DIR}/(?P.*?)/(?P.*?)$') filtered_logs_vector: inputs: @@ -686,17 +363,17 @@ commands: - extended_logs_* type: remap source: | - .namespace = "__NAMESPACE__" - .cluster = "test-kafka" - .role = "broker" - .roleGroup = "default" + .namespace = "${NAMESPACE}" + .cluster = "${CLUSTER_NAME}" + .role = "${ROLE_NAME}" + .roleGroup = "${ROLE_GROUP_NAME}" sinks: aggregator: inputs: - extended_logs type: vector - address: $VECTOR_AGGREGATOR_ADDRESS + address: ${VECTOR_AGGREGATOR_ADDRESS} {% endif %} YAMLEOF )