diff --git a/CHANGELOG.md b/CHANGELOG.md index 71eb21d69..56be97136 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ All notable changes to this project will be documented in this file. - Bump `stackable-operator` to 0.111.0 and `kube` to 3.1.0 ([#878], [#884]). - Internal operator refactoring: introduce dereference() and validate() steps in the reconciler ([#889]). - test: Bump vector-aggregator to 0.55.0, replace /graphql call with gRPC call ([#895]). +- BREAKING: Removed product-config machinery. This 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 ([#897]). ### Fixed @@ -38,6 +40,7 @@ All notable changes to this project will be documented in this file. [#884]: https://github.com/stackabletech/trino-operator/pull/884 [#889]: https://github.com/stackabletech/trino-operator/pull/889 [#895]: https://github.com/stackabletech/trino-operator/pull/895 +[#897]: https://github.com/stackabletech/trino-operator/pull/897 ## [26.3.0] - 2026-03-16 diff --git a/Cargo.lock b/Cargo.lock index 9227a96e7..b231c8c6f 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.60" +version = "1.2.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +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", @@ -331,9 +331,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -353,14 +353,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -392,11 +392,12 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const_format" -version = "0.2.35" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" dependencies = [ "const_format_proc_macros", + "konst", ] [[package]] @@ -519,7 +520,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -530,7 +531,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -541,7 +542,7 @@ checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -565,7 +566,7 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -573,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" @@ -595,7 +593,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -612,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]] @@ -670,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" @@ -734,7 +732,7 @@ checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -906,7 +904,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -923,9 +921,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" @@ -982,15 +980,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]] @@ -1024,9 +1021,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", @@ -1054,9 +1051,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" @@ -1086,9 +1083,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", @@ -1137,9 +1134,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", @@ -1335,9 +1332,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", @@ -1350,7 +1347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", ] [[package]] @@ -1368,16 +1365,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" @@ -1412,9 +1399,9 @@ dependencies = [ [[package]] name = "jiff" -version = "0.2.23" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -1422,18 +1409,18 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-link", ] [[package]] name = "jiff-static" -version = "0.2.23" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1463,26 +1450,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]] @@ -1524,13 +1511,28 @@ dependencies = [ [[package]] name = "k8s-version" version = "0.1.3" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.0#b7c8a3a5483b4d35d0abfa11f6db6c153bda8a51" +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]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + [[package]] name = "kube" version = "3.1.0" @@ -1609,7 +1611,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1650,15 +1652,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.185" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +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", @@ -1674,9 +1676,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", @@ -1701,9 +1703,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" @@ -1722,9 +1724,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" @@ -1744,9 +1746,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", @@ -1773,16 +1775,16 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "smallvec", "zeroize", ] [[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" @@ -1834,9 +1836,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", @@ -1848,9 +1850,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", @@ -1860,9 +1862,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", @@ -1873,9 +1875,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", @@ -1887,14 +1889,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", @@ -1905,21 +1907,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", @@ -2031,7 +2034,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2046,22 +2049,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]] @@ -2105,9 +2108,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] @@ -2181,9 +2184,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", @@ -2191,15 +2194,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]] @@ -2219,9 +2231,9 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "rand_chacha 0.3.1", "rand_core 0.6.4", @@ -2301,14 +2313,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", @@ -2329,9 +2341,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" @@ -2341,9 +2353,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", @@ -2359,9 +2371,6 @@ dependencies = [ "log", "percent-encoding", "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", "sync_wrapper", "tokio", "tower", @@ -2443,7 +2452,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.117", + "syn 2.0.118", "unicode-ident", ] @@ -2458,9 +2467,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.38" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "log", "once_cell", @@ -2473,9 +2482,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", @@ -2485,9 +2494,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "zeroize", ] @@ -2547,7 +2556,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2645,7 +2654,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2656,14 +2665,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", @@ -2741,9 +2750,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" @@ -2779,9 +2788,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" @@ -2804,11 +2813,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]] @@ -2831,26 +2840,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", @@ -2881,7 +2890,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.0#b7c8a3a5483b4d35d0abfa11f6db6c153bda8a51" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "const-oid", "ecdsa", @@ -2893,7 +2902,7 @@ dependencies = [ "rsa", "sha2", "signature", - "snafu 0.9.0", + "snafu 0.9.1", "stackable-shared", "tokio", "tokio-rustls", @@ -2904,8 +2913,8 @@ dependencies = [ [[package]] name = "stackable-operator" -version = "0.111.0" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.0#b7c8a3a5483b4d35d0abfa11f6db6c153bda8a51" +version = "0.111.1" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "base64", "clap", @@ -2917,6 +2926,7 @@ dependencies = [ "futures 0.3.32", "http", "indexmap", + "java-properties", "jiff", "json-patch", "k8s-openapi", @@ -2929,7 +2939,8 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "snafu 0.9.0", + "sha2", + "snafu 0.9.1", "stackable-operator-derive", "stackable-shared", "stackable-telemetry", @@ -2941,23 +2952,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.0#b7c8a3a5483b4d35d0abfa11f6db6c153bda8a51" +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.0#b7c8a3a5483b4d35d0abfa11f6db6c153bda8a51" +version = "0.1.1" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "jiff", "k8s-openapi", @@ -2966,15 +2979,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.0#b7c8a3a5483b4d35d0abfa11f6db6c153bda8a51" +version = "0.6.4" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "axum", "clap", @@ -2985,7 +2998,7 @@ dependencies = [ "opentelemetry-semantic-conventions", "opentelemetry_sdk", "pin-project", - "snafu 0.9.0", + "snafu 0.9.1", "strum", "tokio", "tower", @@ -3006,12 +3019,11 @@ dependencies = [ "const_format", "futures 0.3.32", "indoc", - "product-config", "rstest", "serde", "serde_json", "serde_yaml", - "snafu 0.9.0", + "snafu 0.9.1", "stackable-operator", "strum", "tokio", @@ -3021,21 +3033,21 @@ dependencies = [ [[package]] name = "stackable-versioned" version = "0.10.0" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.0#b7c8a3a5483b4d35d0abfa11f6db6c153bda8a51" +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.0#b7c8a3a5483b4d35d0abfa11f6db6c153bda8a51" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "convert_case", "convert_case_extras", @@ -3047,13 +3059,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.0#b7c8a3a5483b4d35d0abfa11f6db6c153bda8a51" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "arc-swap", "async-trait", @@ -3069,7 +3081,7 @@ dependencies = [ "rand 0.9.4", "serde", "serde_json", - "snafu 0.9.0", + "snafu 0.9.1", "stackable-certs", "stackable-shared", "stackable-telemetry", @@ -3106,7 +3118,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3115,6 +3127,12 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "1.0.109" @@ -3128,9 +3146,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", @@ -3154,7 +3172,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3183,7 +3201,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3194,7 +3212,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3208,12 +3226,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", @@ -3223,15 +3240,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", @@ -3265,14 +3282,14 @@ checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "tokio" -version = "1.52.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -3293,7 +3310,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3342,9 +3359,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", @@ -3363,9 +3380,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", @@ -3390,15 +3407,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" @@ -3420,9 +3448,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", @@ -3430,13 +3458,13 @@ dependencies = [ "futures-util", "http", "http-body", - "iri-string", "mime", "pin-project-lite", "tower", "tower-layer", "tower-service", "tracing", + "url", ] [[package]] @@ -3465,11 +3493,12 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" dependencies = [ "crossbeam-channel", + "symlink", "thiserror 2.0.18", "time", "tracing-subscriber", @@ -3483,7 +3512,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3509,9 +3538,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", @@ -3562,9 +3591,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ucd-trie" @@ -3580,9 +3609,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" @@ -3627,6 +3656,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" @@ -3662,18 +3701,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +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", @@ -3684,9 +3723,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", @@ -3694,9 +3733,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", @@ -3704,31 +3743,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", @@ -3765,7 +3804,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3776,7 +3815,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3887,18 +3926,18 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen" -version = "0.51.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" @@ -3922,15 +3961,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", @@ -3945,35 +3984,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", ] @@ -3986,28 +4025,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]] @@ -4040,7 +4079,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 4ae838783..1400e6072 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.60"; + version = "1.2.64"; edition = "2018"; - sha256 = "084a8ziprdlyrj865f3303qr0b7aaggilkl18slncss6m4yp1ia3"; + 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"; @@ -1060,10 +1061,10 @@ rec { }; "clap" = rec { crateName = "clap"; - version = "4.6.0"; + version = "4.6.1"; edition = "2024"; crateBin = []; - sha256 = "0l8k0ja5rf4hpn2g98bqv5m6lkh2q6b6likjpmm6fjw3cxdsz4xi"; + sha256 = "0lcf88l7vlg796rrqr7wipbbmfa5sgsgx4211b7xmxxv8dz13nqx"; dependencies = [ { name = "clap_builder"; @@ -1141,9 +1142,9 @@ rec { }; "clap_derive" = rec { crateName = "clap_derive"; - version = "4.6.0"; + version = "4.6.1"; edition = "2024"; - sha256 = "0snapc468s7n3avr33dky4y7rmb7ha3qsp9l0k5vh6jacf5bs40i"; + sha256 = "1acpz49hi00iv9jkapixjzcv7s51x8qkfaqscjm36rqgf428dkpj"; procMacro = true; dependencies = [ { @@ -1160,7 +1161,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" ]; } ]; @@ -1226,9 +1227,9 @@ rec { }; "const_format" = rec { crateName = "const_format"; - version = "0.2.35"; + version = "0.2.36"; edition = "2021"; - sha256 = "1b9h03z3k76ail1ldqxcqmsc4raa7dwgwwqwrjf6wmism5lp9akz"; + sha256 = "07ncczs8yndga2f8p4386c827l4fxwzl0pbwp7ijnhcsmlbsd0a4"; authors = [ "rodrimati1992 " ]; @@ -1237,6 +1238,12 @@ rec { name = "const_format_proc_macros"; packageId = "const_format_proc_macros"; } + { + name = "konst"; + packageId = "konst"; + usesDefaultFeatures = false; + features = [ "rust_1_64" ]; + } ]; features = { "__debug" = [ "const_format_proc_macros/debug" ]; @@ -1250,10 +1257,9 @@ rec { "constant_time_as_str" = [ "fmt" ]; "derive" = [ "fmt" "const_format_proc_macros/derive" ]; "fmt" = [ "rust_1_83" ]; - "konst" = [ "dep:konst" ]; "more_str_macros" = [ "rust_1_64" ]; "nightly_const_generics" = [ "const_generics" ]; - "rust_1_64" = [ "rust_1_51" "konst" "konst/rust_1_64" ]; + "rust_1_64" = [ "rust_1_51" ]; "rust_1_83" = [ "rust_1_64" ]; }; resolvedDefaultFeatures = [ "default" ]; @@ -1586,7 +1592,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" "extra-traits" ]; } ]; @@ -1617,7 +1623,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; @@ -1643,7 +1649,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" "visit-mut" ]; } ]; @@ -1720,7 +1726,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "extra-traits" ]; } ]; @@ -1734,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" ]; @@ -1753,7 +1751,7 @@ rec { "rand09" = [ "dep:rand09" ]; "serde" = [ "dep:serde_core" ]; }; - resolvedDefaultFeatures = [ "default" "powerfmt" ]; + resolvedDefaultFeatures = [ "default" ]; }; "derive_more" = rec { crateName = "derive_more"; @@ -1822,7 +1820,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; buildDependencies = [ @@ -1901,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 " @@ -1919,7 +1917,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; features = { @@ -2085,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" ]; } ]; @@ -2103,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" ]; @@ -2292,7 +2287,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; features = { @@ -2793,7 +2788,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" ]; } ]; @@ -2825,9 +2820,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 " @@ -3085,9 +3080,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 " @@ -3109,17 +3104,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" ]; @@ -3209,9 +3201,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 " @@ -3322,14 +3314,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" ]; @@ -3404,9 +3393,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 " @@ -3523,9 +3512,9 @@ rec { }; "hyper" = rec { crateName = "hyper"; - version = "1.9.0"; + version = "1.10.1"; edition = "2021"; - sha256 = "1jmwbwqcaficskg76kq402gbymbnh2z4v99xwq3l5aa6n8bg16b2"; + sha256 = "1624nwrh1ci34psqcl3q8q266kha8kd6fmqjj14qck49l59iqa2m"; authors = [ "Sean McArthur " ]; @@ -4296,9 +4285,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" ]; @@ -4332,7 +4321,7 @@ rec { } { name = "hashbrown"; - packageId = "hashbrown 0.17.0"; + packageId = "hashbrown 0.17.1"; usesDefaultFeatures = false; } ]; @@ -4390,39 +4379,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"; @@ -4492,9 +4448,9 @@ rec { }; "jiff" = rec { crateName = "jiff"; - version = "0.2.23"; + version = "0.2.28"; edition = "2021"; - sha256 = "0nc37n7jvgrzxdkcgc2hsfdf70lfagigjalh4igjrm5njvf4cd8s"; + sha256 = "00lixngcc7amh2fcsxfr0z38j06lllhapz192biv1qj97q1x60s6"; authors = [ "Andrew Gallant " ]; @@ -4540,12 +4496,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 = [ @@ -4564,7 +4518,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" ]; @@ -4574,9 +4528,9 @@ rec { }; "jiff-static" = rec { crateName = "jiff-static"; - version = "0.2.23"; + version = "0.2.28"; edition = "2021"; - sha256 = "192ss3cnixvg79cpa76clwkhn4mmz10vnwsbf7yjw8i484s8p31a"; + sha256 = "0irbhfh2f4i9w5l53jcmh6ssnhdd92wfy76978chgwnxilvk4bbq"; procMacro = true; libName = "jiff_static"; authors = [ @@ -4593,7 +4547,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; features = { @@ -4656,9 +4610,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" @@ -4667,7 +4621,6 @@ rec { { name = "cfg-if"; packageId = "cfg-if"; - optional = true; } { name = "futures-util"; @@ -4676,11 +4629,6 @@ rec { usesDefaultFeatures = false; features = [ "std" ]; } - { - name = "once_cell"; - packageId = "once_cell"; - usesDefaultFeatures = false; - } { name = "wasm-bindgen"; packageId = "wasm-bindgen"; @@ -4689,17 +4637,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 " @@ -4709,6 +4656,11 @@ rec { name = "jsonptr"; packageId = "jsonptr"; } + { + name = "schemars"; + packageId = "schemars"; + optional = true; + } { name = "serde"; packageId = "serde"; @@ -4720,10 +4672,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"; @@ -4735,7 +4691,7 @@ rec { "schemars" = [ "dep:schemars" ]; "utoipa" = [ "dep:utoipa" ]; }; - resolvedDefaultFeatures = [ "default" "diff" ]; + resolvedDefaultFeatures = [ "default" "diff" "schemars" ]; }; "jsonpath-rust" = rec { crateName = "jsonpath-rust"; @@ -4861,9 +4817,9 @@ rec { edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "b7c8a3a5483b4d35d0abfa11f6db6c153bda8a51"; - sha256 = "14q10sppdjdf3vbcbxz12rlgm1g9l6p87nk9wr707w2a71z8vgxc"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; libName = "k8s_version"; authors = [ @@ -4881,7 +4837,7 @@ rec { } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } ]; features = { @@ -4890,6 +4846,53 @@ rec { }; resolvedDefaultFeatures = [ "darling" ]; }; + "konst" = rec { + crateName = "konst"; + version = "0.2.20"; + edition = "2018"; + sha256 = "1yyf1fhk28wbf1lqrga9as4cpfmpbry9a5vvdqyxgz14g3nk708j"; + authors = [ + "rodrimati1992 " + ]; + dependencies = [ + { + name = "konst_macro_rules"; + packageId = "konst_macro_rules"; + } + ]; + features = { + "__ui" = [ "__test" "trybuild" "rust_latest_stable" ]; + "const_generics" = [ "rust_1_51" ]; + "constant_time_slice" = [ "rust_latest_stable" ]; + "default" = [ "cmp" "parsing" ]; + "deref_raw_in_fn" = [ "rust_1_56" ]; + "konst_proc_macros" = [ "dep:konst_proc_macros" ]; + "mut_refs" = [ "rust_latest_stable" "konst_macro_rules/mut_refs" ]; + "nightly_mut_refs" = [ "mut_refs" "konst_macro_rules/nightly_mut_refs" ]; + "parsing" = [ "parsing_no_proc" "konst_proc_macros" ]; + "rust_1_51" = [ "konst_macro_rules/rust_1_51" ]; + "rust_1_55" = [ "rust_1_51" "konst_macro_rules/rust_1_55" ]; + "rust_1_56" = [ "rust_1_55" "konst_macro_rules/rust_1_56" ]; + "rust_1_57" = [ "rust_1_56" "konst_macro_rules/rust_1_57" ]; + "rust_1_61" = [ "rust_1_57" "konst_macro_rules/rust_1_61" ]; + "rust_1_64" = [ "rust_1_61" ]; + "rust_latest_stable" = [ "rust_1_64" ]; + "trybuild" = [ "dep:trybuild" ]; + }; + resolvedDefaultFeatures = [ "rust_1_51" "rust_1_55" "rust_1_56" "rust_1_57" "rust_1_61" "rust_1_64" ]; + }; + "konst_macro_rules" = rec { + crateName = "konst_macro_rules"; + version = "0.2.19"; + edition = "2018"; + sha256 = "0dswja0dqcww4x3fwjnirc0azv2n6cazn8yv0kddksd8awzkz4x4"; + authors = [ + "rodrimati1992 " + ]; + features = { + }; + resolvedDefaultFeatures = [ "rust_1_51" "rust_1_55" "rust_1_56" "rust_1_57" "rust_1_61" ]; + }; "kube" = rec { crateName = "kube"; version = "3.1.0"; @@ -5309,7 +5312,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "extra-traits" ]; } ]; @@ -5466,9 +5469,9 @@ rec { }; "libc" = rec { crateName = "libc"; - version = "0.2.185"; + version = "0.2.186"; edition = "2021"; - sha256 = "13rbdaa59l3w92q7kfcxx8zbikm99zzw54h59aqvcv5wx47jrzsj"; + sha256 = "0rnyhzjyqq9x56skkllbjzzzwym3r61lq3l4hqj64v71gw0r3av8"; authors = [ "The Rust Project Developers" ]; @@ -5482,10 +5485,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 = [ @@ -5543,10 +5546,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 " @@ -5623,9 +5626,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" ]; @@ -5679,9 +5682,9 @@ rec { }; "memchr" = rec { crateName = "memchr"; - version = "2.8.0"; + version = "2.8.2"; edition = "2021"; - sha256 = "0y9zzxcqxvdqg6wyag7vc3h0blhdn7hkq164bxyx2vph8zs5ijpq"; + sha256 = "1i33wr49pcz2sbd12nds3n9fszay8kq5bk78gwciz462mcs49448"; authors = [ "Andrew Gallant " "bluss" @@ -5742,9 +5745,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 " @@ -5843,7 +5846,7 @@ rec { } { name = "rand"; - packageId = "rand 0.8.5"; + packageId = "rand 0.8.6"; optional = true; usesDefaultFeatures = false; } @@ -5862,7 +5865,7 @@ rec { devDependencies = [ { name = "rand"; - packageId = "rand 0.8.5"; + packageId = "rand 0.8.6"; features = [ "small_rng" ]; } ]; @@ -5880,9 +5883,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 " @@ -6015,9 +6018,9 @@ rec { }; "opentelemetry" = rec { crateName = "opentelemetry"; - version = "0.31.0"; + version = "0.32.0"; edition = "2021"; - sha256 = "18629xsj4rsyiby9aj511q6wcw6s9m09gx3ymw1yjcvix1mcsjxq"; + sha256 = "10ln14d1jgc8rvw97mblc9blzcgpg1bimim4d170b7ia4mijq55h"; dependencies = [ { name = "futures-core"; @@ -6054,24 +6057,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 = [ { @@ -6114,18 +6117,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 = [ { @@ -6161,16 +6161,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 = [ { @@ -6231,10 +6231,9 @@ rec { usesDefaultFeatures = false; } { - name = "tracing"; - packageId = "tracing"; + name = "tonic-types"; + packageId = "tonic-types"; optional = true; - usesDefaultFeatures = false; } ]; devDependencies = [ @@ -6259,16 +6258,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" ]; @@ -6281,27 +6283,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 = [ { @@ -6345,30 +6347,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 = { }; @@ -6376,9 +6377,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"; @@ -6404,6 +6405,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"; @@ -6428,10 +6436,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" ]; @@ -6448,15 +6464,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"; @@ -6791,7 +6806,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; features = { @@ -6830,9 +6845,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 = [ { @@ -6844,9 +6859,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 = [ @@ -6860,7 +6875,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" ]; } @@ -6965,13 +6980,13 @@ rec { "default" = [ "fallback" ]; "serde" = [ "dep:serde" ]; }; - resolvedDefaultFeatures = [ "require-cas" ]; + resolvedDefaultFeatures = [ "fallback" "require-cas" ]; }; "portable-atomic-util" = rec { crateName = "portable-atomic-util"; - version = "0.2.6"; + version = "0.2.7"; edition = "2018"; - sha256 = "18wrsx7fjwc2kgbpfjfm3igv3vdzsidmjhbqivjln7d0c6z9f4q9"; + sha256 = "0616j0fhy6y71hyxg3n86f6hng0fmsc269s3wp4gl8ww4p8hd8f2"; libName = "portable_atomic_util"; dependencies = [ { @@ -6982,6 +6997,7 @@ rec { } ]; features = { + "serde" = [ "dep:serde" ]; "std" = [ "alloc" ]; }; resolvedDefaultFeatures = [ "alloc" ]; @@ -7167,9 +7183,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 " @@ -7196,9 +7212,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 = [ @@ -7226,12 +7242,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"; @@ -7265,11 +7309,11 @@ rec { "rustc-dep-of-std" = [ "core" ]; }; }; - "rand 0.8.5" = rec { + "rand 0.8.6" = rec { crateName = "rand"; - version = "0.8.5"; + version = "0.8.6"; edition = "2018"; - sha256 = "013l6931nn7gkc23jz5mm3qdhf93jjf0fg64nz2lp4i51qd8vbrl"; + sha256 = "12kd4rljn86m00rcaz4c1rcya4mb4gk5ig6i8xq00a8wjgxfr82w"; authors = [ "The Rand Project Developers" "The Rust Project Developers" @@ -7291,12 +7335,9 @@ rec { "default" = [ "std" "std_rng" ]; "getrandom" = [ "rand_core/getrandom" ]; "libc" = [ "dep:libc" ]; - "log" = [ "dep:log" ]; - "packed_simd" = [ "dep:packed_simd" ]; "rand_chacha" = [ "dep:rand_chacha" ]; "serde" = [ "dep:serde" ]; "serde1" = [ "serde" "rand_core/serde1" ]; - "simd_support" = [ "packed_simd" ]; "std" = [ "rand_core/std" "rand_chacha/std" "alloc" "getrandom" "libc" ]; "std_rng" = [ "rand_chacha" ]; }; @@ -7507,16 +7548,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 " @@ -7633,9 +7674,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" @@ -7664,9 +7705,9 @@ rec { }; "reqwest" = rec { crateName = "reqwest"; - version = "0.12.28"; + version = "0.13.4"; edition = "2021"; - sha256 = "0iqidijghgqbzl3bjg5hb4zmigwa4r612bgi0yiq0c90b6jkrpgd"; + sha256 = "1hy1plns9krbh3h1dy2sdjygsfkdcnxm6pbxdi0ya9b5vq8mi711"; authors = [ "Sean McArthur " ]; @@ -7683,7 +7724,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"; @@ -7703,62 +7744,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"; @@ -7769,27 +7792,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"; @@ -7798,17 +7821,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" ]; } ]; @@ -7817,33 +7840,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" ]; } { @@ -7855,40 +7872,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" ]; @@ -8165,7 +8179,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" "parsing" "extra-traits" "visit" "visit-mut" ]; } { @@ -8200,9 +8214,9 @@ rec { }; "rustls" = rec { crateName = "rustls"; - version = "0.23.38"; + version = "0.23.40"; edition = "2021"; - sha256 = "089ssmhd79f0kd22brh6lkaadql2p3pi6579ax1s0kn1n9pldyb9"; + sha256 = "12qnv3ag4wrw7aj8jng74kgrilpjm2b1rfcjaac8h691frccv1pg"; dependencies = [ { name = "log"; @@ -8269,9 +8283,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 = [ { @@ -8300,9 +8314,9 @@ rec { }; "rustls-pki-types" = rec { crateName = "rustls-pki-types"; - version = "1.14.0"; + version = "1.14.1"; edition = "2021"; - sha256 = "1p9zsgslvwzzkzhm6bqicffqndr4jpx67992b0vl0pi21a5hy15y"; + sha256 = "1a9pr54y0f3qr97bxpd3ahjldq0gqdld0h799xbnwdzbwxx1k9rh"; libName = "rustls_pki_types"; dependencies = [ { @@ -8507,13 +8521,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" ]; } ]; @@ -8797,7 +8811,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; usesDefaultFeatures = false; features = [ "clone-impls" "derive" "parsing" "printing" "proc-macro" ]; } @@ -8829,7 +8843,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; usesDefaultFeatures = false; features = [ "clone-impls" "derive" "parsing" "printing" ]; } @@ -8838,9 +8852,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 " @@ -9081,9 +9095,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 " @@ -9178,9 +9192,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" ]; @@ -9262,29 +9276,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" ]; }; @@ -9344,7 +9354,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" ]; } ]; @@ -9352,11 +9362,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 = [ @@ -9380,9 +9390,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 = { @@ -9390,9 +9400,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 " @@ -9490,9 +9500,9 @@ rec { edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "b7c8a3a5483b4d35d0abfa11f6db6c153bda8a51"; - sha256 = "14q10sppdjdf3vbcbxz12rlgm1g9l6p87nk9wr707w2a71z8vgxc"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; libName = "stackable_certs"; authors = [ @@ -9550,7 +9560,7 @@ rec { } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } { name = "stackable-shared"; @@ -9589,13 +9599,13 @@ rec { }; "stackable-operator" = rec { crateName = "stackable-operator"; - version = "0.111.0"; + version = "0.111.1"; edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "b7c8a3a5483b4d35d0abfa11f6db6c153bda8a51"; - sha256 = "14q10sppdjdf3vbcbxz12rlgm1g9l6p87nk9wr707w2a71z8vgxc"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; libName = "stackable_operator"; authors = [ @@ -9645,6 +9655,10 @@ rec { name = "indexmap"; packageId = "indexmap"; } + { + name = "java-properties"; + packageId = "java-properties"; + } { name = "jiff"; packageId = "jiff"; @@ -9652,6 +9666,7 @@ rec { { name = "json-patch"; packageId = "json-patch"; + features = [ "schemars" ]; } { name = "k8s-openapi"; @@ -9699,9 +9714,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"; @@ -9755,12 +9775,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" ]; @@ -9773,9 +9802,9 @@ rec { edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "b7c8a3a5483b4d35d0abfa11f6db6c153bda8a51"; - sha256 = "14q10sppdjdf3vbcbxz12rlgm1g9l6p87nk9wr707w2a71z8vgxc"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; procMacro = true; libName = "stackable_operator_derive"; @@ -9797,20 +9826,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 = "b7c8a3a5483b4d35d0abfa11f6db6c153bda8a51"; - sha256 = "14q10sppdjdf3vbcbxz12rlgm1g9l6p87nk9wr707w2a71z8vgxc"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; libName = "stackable_shared"; authors = [ @@ -9854,7 +9883,7 @@ rec { } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } { name = "strum"; @@ -9885,13 +9914,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 = "b7c8a3a5483b4d35d0abfa11f6db6c153bda8a51"; - sha256 = "14q10sppdjdf3vbcbxz12rlgm1g9l6p87nk9wr707w2a71z8vgxc"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; libName = "stackable_telemetry"; authors = [ @@ -9934,7 +9963,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"; @@ -9942,7 +9971,7 @@ rec { } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } { name = "strum"; @@ -10034,10 +10063,6 @@ rec { name = "indoc"; packageId = "indoc"; } - { - name = "product-config"; - packageId = "product-config"; - } { name = "serde"; packageId = "serde"; @@ -10053,7 +10078,7 @@ rec { } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } { name = "stackable-operator"; @@ -10100,9 +10125,9 @@ rec { edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "b7c8a3a5483b4d35d0abfa11f6db6c153bda8a51"; - sha256 = "14q10sppdjdf3vbcbxz12rlgm1g9l6p87nk9wr707w2a71z8vgxc"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; libName = "stackable_versioned"; authors = [ @@ -10135,7 +10160,7 @@ rec { } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } { name = "stackable-versioned-macros"; @@ -10150,9 +10175,9 @@ rec { edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "b7c8a3a5483b4d35d0abfa11f6db6c153bda8a51"; - sha256 = "14q10sppdjdf3vbcbxz12rlgm1g9l6p87nk9wr707w2a71z8vgxc"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; procMacro = true; libName = "stackable_versioned_macros"; @@ -10207,7 +10232,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; @@ -10218,9 +10243,9 @@ rec { edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "b7c8a3a5483b4d35d0abfa11f6db6c153bda8a51"; - sha256 = "14q10sppdjdf3vbcbxz12rlgm1g9l6p87nk9wr707w2a71z8vgxc"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; libName = "stackable_webhook"; authors = [ @@ -10292,7 +10317,7 @@ rec { } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } { name = "stackable-certs"; @@ -10402,7 +10427,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "parsing" ]; } ]; @@ -10422,6 +10447,16 @@ rec { }; resolvedDefaultFeatures = [ "i128" ]; }; + "symlink" = rec { + crateName = "symlink"; + version = "0.1.0"; + edition = "2015"; + sha256 = "02h1i0b81mxb4vns4xrvrfibpcvs7jqqav8p3yilwik8cv73r5x7"; + authors = [ + "Chris Morgan " + ]; + + }; "syn 1.0.109" = rec { crateName = "syn"; version = "1.0.109"; @@ -10456,11 +10491,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 " ]; @@ -10532,7 +10567,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" ]; } @@ -10599,7 +10634,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; @@ -10625,7 +10660,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; @@ -10649,9 +10684,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" @@ -10660,12 +10695,6 @@ rec { { name = "deranged"; packageId = "deranged"; - features = [ "powerfmt" ]; - } - { - name = "itoa"; - packageId = "itoa"; - optional = true; } { name = "num-conv"; @@ -10705,13 +10734,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" ]; @@ -10724,9 +10754,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 " @@ -10737,9 +10767,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 = [ @@ -10842,7 +10872,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "parsing" ]; } ]; @@ -10854,9 +10884,9 @@ rec { }; "tokio" = rec { crateName = "tokio"; - version = "1.52.0"; + version = "1.52.3"; edition = "2021"; - sha256 = "0xnpygq9578c8rqjgkj5bj8pgfx9zj337kvk3v4kigqwkgska4d9"; + sha256 = "1zpzazypkg61sw91na1m85x5s4rsjym335fwwhwm1hcs70dz1iwg"; authors = [ "Tokio Contributors " ]; @@ -10994,7 +11024,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" ]; } ]; @@ -11166,9 +11196,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"; @@ -11221,9 +11251,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 " ]; @@ -11350,9 +11380,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 " @@ -11373,6 +11403,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"; @@ -11494,9 +11551,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 " @@ -11530,11 +11587,6 @@ rec { packageId = "http-body"; optional = true; } - { - name = "iri-string"; - packageId = "iri-string"; - optional = true; - } { name = "mime"; packageId = "mime"; @@ -11564,6 +11616,11 @@ rec { optional = true; usesDefaultFeatures = false; } + { + name = "url"; + packageId = "url"; + optional = true; + } ]; devDependencies = [ { @@ -11585,35 +11642,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" ]; @@ -11622,7 +11677,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"; @@ -11695,9 +11750,9 @@ rec { }; "tracing-appender" = rec { crateName = "tracing-appender"; - version = "0.2.4"; + version = "0.2.5"; edition = "2018"; - sha256 = "1bxf7xvsr89glbq174cx0b9pinaacbhlmc85y1ssniv2rq5lhvbq"; + sha256 = "0g4a6q5s3wafid5lqw1ljzvh1nhk3a4zmb627fxv96dr7qcqc1h5"; libName = "tracing_appender"; authors = [ "Zeki Sherif " @@ -11708,6 +11763,10 @@ rec { name = "crossbeam-channel"; packageId = "crossbeam-channel"; } + { + name = "symlink"; + packageId = "symlink"; + } { name = "thiserror"; packageId = "thiserror 2.0.18"; @@ -11752,7 +11811,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" ]; } @@ -11825,9 +11884,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 = [ { @@ -12062,13 +12121,9 @@ rec { }; "typenum" = rec { crateName = "typenum"; - version = "1.19.0"; + version = "1.20.1"; edition = "2018"; - sha256 = "1fw2mpbn2vmqan56j1b3fbpcdg80mz26fm53fs16bq5xcq84hban"; - authors = [ - "Paho Lurie-Gregg " - "Andre Bogus " - ]; + sha256 = "086s9ly0906kw5yw41249fba97w5zfxf03pyfwdkffvcprqfixdn"; features = { "scale-info" = [ "dep:scale-info" ]; "scale_info" = [ "scale-info/derive" ]; @@ -12101,9 +12156,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 " @@ -12229,6 +12284,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"; @@ -12296,9 +12411,9 @@ rec { }; "wasip2" = rec { crateName = "wasip2"; - version = "1.0.2+wasi-0.2.9"; + version = "1.0.4+wasi-0.2.12"; edition = "2021"; - sha256 = "1xdw7v08jpfjdg94sp4lbdgzwa587m5ifpz6fpdnkh02kwizj5wm"; + sha256 = "11wl7lqwq4pbmlmzr6n7bwz0hzy1z6sxc4554bkmrr86w4vznzmn"; dependencies = [ { name = "wit-bindgen"; @@ -12316,9 +12431,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" @@ -12367,9 +12482,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" @@ -12379,7 +12494,6 @@ rec { name = "js-sys"; packageId = "js-sys"; usesDefaultFeatures = false; - features = [ "futures" ]; } { name = "wasm-bindgen"; @@ -12396,9 +12510,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 = [ @@ -12420,9 +12534,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" @@ -12442,7 +12556,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "visit" "visit-mut" "full" "extra-traits" ]; } { @@ -12456,10 +12570,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" @@ -12474,9 +12588,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" @@ -12560,6 +12674,7 @@ rec { "CssStyleSheet" = [ "StyleSheet" ]; "CssSupportsRule" = [ "CssConditionRule" "CssGroupingRule" "CssRule" ]; "CssTransition" = [ "Animation" "EventTarget" ]; + "CssViewTransitionRule" = [ "CssRule" ]; "CustomEvent" = [ "Event" ]; "DedicatedWorkerGlobalScope" = [ "EventTarget" "WorkerGlobalScope" ]; "DelayNode" = [ "AudioNode" "EventTarget" ]; @@ -13050,7 +13165,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; usesDefaultFeatures = false; features = [ "parsing" "proc-macro" "printing" "full" "clone-impls" ]; } @@ -13077,7 +13192,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; usesDefaultFeatures = false; features = [ "parsing" "proc-macro" "printing" "full" "clone-impls" ]; } @@ -13636,7 +13751,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"; @@ -13773,9 +13888,9 @@ rec { }; "winnow" = rec { crateName = "winnow"; - version = "1.0.1"; + version = "1.0.3"; edition = "2021"; - sha256 = "1dbji1bwviy08pl74f2qw2m4w9hc4p3vyl3lfj05jdydy59w1nh9"; + sha256 = "1wajycd3krn6h699vydjv7hm0ll5l31p899qzpk59y2is74y34h5"; dependencies = [ { name = "memchr"; @@ -13798,19 +13913,20 @@ rec { }; "wit-bindgen" = rec { crateName = "wit-bindgen"; - version = "0.51.0"; + version = "0.57.1"; edition = "2024"; - sha256 = "19fazgch8sq5cvjv3ynhhfh5d5x08jq2pkw8jfb05vbcyqcr496p"; + sha256 = "0vjk2jb593ri9k1aq4iqs2si9mrw5q46wxnn78im7hm7hx799gqy"; libName = "wit_bindgen"; authors = [ "Alex Crichton " ]; features = { - "async" = [ "std" "wit-bindgen-rust-macro?/async" ]; - "async-spawn" = [ "async" "dep:futures" ]; + "async-spawn" = [ "async" "dep:futures" "std" ]; "bitflags" = [ "dep:bitflags" ]; - "default" = [ "macros" "realloc" "async" "std" "bitflags" ]; + "default" = [ "macros" "realloc" "async" "std" "bitflags" "macro-string" ]; + "futures-stream" = [ "async" "dep:futures" ]; "inter-task-wakeup" = [ "async" ]; + "macro-string" = [ "wit-bindgen-rust-macro?/macro-string" ]; "macros" = [ "dep:wit-bindgen-rust-macro" ]; "rustc-dep-of-std" = [ "dep:core" "dep:alloc" ]; }; @@ -13886,9 +14002,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)" @@ -13897,9 +14013,9 @@ rec { }; "yoke" = rec { crateName = "yoke"; - version = "0.8.2"; + version = "0.8.3"; edition = "2021"; - sha256 = "1jprcs7a98a5whvfs6r3jvfh1nnfp6zyijl7y4ywmn88lzywbs5b"; + sha256 = "1xgyj6c2lxj2bp891ynmhws87c6z7yyv2li1v0ss9di40hxf57vh"; authors = [ "Manish Goregaokar " ]; @@ -13951,7 +14067,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "fold" ]; } { @@ -13963,9 +14079,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 " @@ -13999,9 +14115,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 = [ @@ -14019,14 +14135,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" ]; } ]; @@ -14034,11 +14150,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 = [ { @@ -14075,7 +14191,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "fold" ]; } { @@ -14087,9 +14203,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" ]; @@ -14111,9 +14227,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" @@ -14129,7 +14245,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" "extra-traits" "visit" ]; } ]; @@ -14242,7 +14358,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 853f9ab82..21bac35db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,13 +10,12 @@ edition = "2024" repository = "https://github.com/stackabletech/trino-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.0", features = ["webhook"] } +stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git", tag = "stackable-operator-0.111.1", features = ["webhook"] } anyhow = "1.0" async-trait = "0.1" built = { version = "0.8", features = ["chrono", "git2"] } -clap = "4.5" +clap = "4.6" const_format = "0.2" futures = { version = "0.3", features = ["compat"] } indoc = "2.0" @@ -26,9 +25,9 @@ serde_json = "1.0" serde_yaml = "0.9" snafu = "0.9" strum = { version = "0.28", features = ["derive"] } -tokio = { version = "1.40", features = ["full"] } +tokio = { version = "1.52", features = ["full"] } tracing = "0.1" -# [patch."https://github.com/stackabletech/operator-rs.git"] -# stackable-operator = { git = "https://github.com/stackabletech//operator-rs.git", branch = "main" } +[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" } diff --git a/crate-hashes.json b/crate-hashes.json index 71fbc1c38..0ff88d858 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.0#k8s-version@0.1.3": "14q10sppdjdf3vbcbxz12rlgm1g9l6p87nk9wr707w2a71z8vgxc", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.0#stackable-certs@0.4.0": "14q10sppdjdf3vbcbxz12rlgm1g9l6p87nk9wr707w2a71z8vgxc", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.0#stackable-operator-derive@0.3.1": "14q10sppdjdf3vbcbxz12rlgm1g9l6p87nk9wr707w2a71z8vgxc", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.0#stackable-operator@0.111.0": "14q10sppdjdf3vbcbxz12rlgm1g9l6p87nk9wr707w2a71z8vgxc", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.0#stackable-shared@0.1.0": "14q10sppdjdf3vbcbxz12rlgm1g9l6p87nk9wr707w2a71z8vgxc", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.0#stackable-telemetry@0.6.3": "14q10sppdjdf3vbcbxz12rlgm1g9l6p87nk9wr707w2a71z8vgxc", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.0#stackable-versioned-macros@0.10.0": "14q10sppdjdf3vbcbxz12rlgm1g9l6p87nk9wr707w2a71z8vgxc", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.0#stackable-versioned@0.10.0": "14q10sppdjdf3vbcbxz12rlgm1g9l6p87nk9wr707w2a71z8vgxc", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.0#stackable-webhook@0.9.1": "14q10sppdjdf3vbcbxz12rlgm1g9l6p87nk9wr707w2a71z8vgxc", + "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 89c7e6b32..9bd8c3b2a 100644 --- a/deploy/config-spec/properties.yaml +++ b/deploy/config-spec/properties.yaml @@ -1,269 +1,5 @@ +--- version: 0.1.0 spec: - units: - - unit: &unitNodeEnvironment - name: "node_environment" - regex: "^[a-z][a-z0-9_]*[a-z0-9]$" - examples: - - "a1_2_3b" - - unit: &unitMemory - name: "memory" - regex: "(^\\p{N}+)(?:\\s*)((?:b|k|m|g|t|p|kb|mb|gb|tb|pb|B|K|M|G|T|P|KB|MB|GB|TB|PB)\\b$)" - examples: - - "1024b" - - "1024kb" - - "500m" - - "1g" - -################################################################################################### -# node.properties -################################################################################################### -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: "coordinator" - 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.ttl" - kind: - type: "file" - file: "security.properties" - datatype: - type: "integer" - min: "0" - recommendedValues: - - fromVersion: "0.0.0" - value: "30" - roles: - - name: "worker" - 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: "coordinator" - 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: - 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: "worker" - 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: &nodeEnvironment - propertyNames: - - name: "node.environment" - kind: - type: "file" - file: "node.properties" - datatype: - type: "string" - unit: *unitNodeEnvironment - roles: - - name: "coordinator" - required: true - - name: "worker" - required: true - asOfVersion: "0.0.0" - -################################################################################################### -# config.properties -################################################################################################### - - property: &coordinator - propertyNames: - - name: "coordinator" - kind: - type: "file" - file: "config.properties" - datatype: - type: "bool" - defaultValues: - - fromVersion: "0.0.0" - value: "false" - roles: - - name: "coordinator" - required: true - - name: "worker" - required: true - asOfVersion: "0.0.0" - - - property: &nodeSchedulerIncludeCoordinator - propertyNames: - - name: "node-scheduler.include-coordinator" - kind: - type: "file" - file: "config.properties" - datatype: - type: "bool" - defaultValues: - - fromVersion: "0.0.0" - value: "false" - roles: - - name: "coordinator" - required: true - asOfVersion: "0.0.0" - - - property: &httpServerHttpPort - propertyNames: - - name: "http-server.http.port" - kind: - type: "file" - file: "config.properties" - datatype: - type: "integer" - min: "1024" - max: "65535" - defaultValues: - - fromVersion: "0.0.0" - value: "8080" - roles: - - name: "coordinator" - required: false - - name: "worker" - required: false - asOfVersion: "0.0.0" - - - property: &httpServerHttpsPort - propertyNames: - - name: "http-server.https.port" - kind: - type: "file" - file: "config.properties" - datatype: - type: "integer" - min: "1024" - max: "65535" - defaultValues: - - fromVersion: "0.0.0" - value: "8443" - roles: - - name: "coordinator" - required: false - - name: "worker" - required: false - asOfVersion: "0.0.0" - - - property: &queryMaxMemory - propertyNames: - - name: "query.max-memory" - kind: - type: "file" - file: "config.properties" - datatype: - type: "string" - unit: *unitMemory - defaultValues: - - fromVersion: "0.0.0" - value: "50GB" - roles: - - name: "coordinator" - required: true - - name: "worker" - required: true - asOfVersion: "0.0.0" - - - property: &queryMaxMemoryPerNode - propertyNames: - - name: "query.max-memory-per-node" - kind: - type: "file" - file: "config.properties" - datatype: - type: "string" - unit: *unitMemory - roles: - - name: "coordinator" - required: false - - name: "worker" - required: false - asOfVersion: "0.0.0" - - - property: &httpServerAuthenticationType - propertyNames: - - name: "http-server.authentication.type" - kind: - type: "file" - file: "config.properties" - datatype: - type: "string" - roles: - - name: "coordinator" - required: false - asOfVersion: "0.0.0" - -################################################################################################### -# jvm.config -################################################################################################### - -################################################################################################### -# log.properties -################################################################################################### - - - property: &ioTrino - propertyNames: - - name: "io.trino" - kind: - type: "file" - file: "log.properties" - datatype: - type: "string" - defaultValues: - - fromVersion: "0.0.0" - value: "INFO" - allowedValues: - - "INFO" - - "DEBUG" - - "WARN" - - "ERROR" - roles: - - name: "coordinator" - required: true - - name: "worker" - required: true - asOfVersion: "0.0.0" + units: [] +properties: [] diff --git a/deploy/helm/trino-operator/configs/properties.yaml b/deploy/helm/trino-operator/configs/properties.yaml index 89c7e6b32..9bd8c3b2a 100644 --- a/deploy/helm/trino-operator/configs/properties.yaml +++ b/deploy/helm/trino-operator/configs/properties.yaml @@ -1,269 +1,5 @@ +--- version: 0.1.0 spec: - units: - - unit: &unitNodeEnvironment - name: "node_environment" - regex: "^[a-z][a-z0-9_]*[a-z0-9]$" - examples: - - "a1_2_3b" - - unit: &unitMemory - name: "memory" - regex: "(^\\p{N}+)(?:\\s*)((?:b|k|m|g|t|p|kb|mb|gb|tb|pb|B|K|M|G|T|P|KB|MB|GB|TB|PB)\\b$)" - examples: - - "1024b" - - "1024kb" - - "500m" - - "1g" - -################################################################################################### -# node.properties -################################################################################################### -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: "coordinator" - 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.ttl" - kind: - type: "file" - file: "security.properties" - datatype: - type: "integer" - min: "0" - recommendedValues: - - fromVersion: "0.0.0" - value: "30" - roles: - - name: "worker" - 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: "coordinator" - 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: - 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: "worker" - 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: &nodeEnvironment - propertyNames: - - name: "node.environment" - kind: - type: "file" - file: "node.properties" - datatype: - type: "string" - unit: *unitNodeEnvironment - roles: - - name: "coordinator" - required: true - - name: "worker" - required: true - asOfVersion: "0.0.0" - -################################################################################################### -# config.properties -################################################################################################### - - property: &coordinator - propertyNames: - - name: "coordinator" - kind: - type: "file" - file: "config.properties" - datatype: - type: "bool" - defaultValues: - - fromVersion: "0.0.0" - value: "false" - roles: - - name: "coordinator" - required: true - - name: "worker" - required: true - asOfVersion: "0.0.0" - - - property: &nodeSchedulerIncludeCoordinator - propertyNames: - - name: "node-scheduler.include-coordinator" - kind: - type: "file" - file: "config.properties" - datatype: - type: "bool" - defaultValues: - - fromVersion: "0.0.0" - value: "false" - roles: - - name: "coordinator" - required: true - asOfVersion: "0.0.0" - - - property: &httpServerHttpPort - propertyNames: - - name: "http-server.http.port" - kind: - type: "file" - file: "config.properties" - datatype: - type: "integer" - min: "1024" - max: "65535" - defaultValues: - - fromVersion: "0.0.0" - value: "8080" - roles: - - name: "coordinator" - required: false - - name: "worker" - required: false - asOfVersion: "0.0.0" - - - property: &httpServerHttpsPort - propertyNames: - - name: "http-server.https.port" - kind: - type: "file" - file: "config.properties" - datatype: - type: "integer" - min: "1024" - max: "65535" - defaultValues: - - fromVersion: "0.0.0" - value: "8443" - roles: - - name: "coordinator" - required: false - - name: "worker" - required: false - asOfVersion: "0.0.0" - - - property: &queryMaxMemory - propertyNames: - - name: "query.max-memory" - kind: - type: "file" - file: "config.properties" - datatype: - type: "string" - unit: *unitMemory - defaultValues: - - fromVersion: "0.0.0" - value: "50GB" - roles: - - name: "coordinator" - required: true - - name: "worker" - required: true - asOfVersion: "0.0.0" - - - property: &queryMaxMemoryPerNode - propertyNames: - - name: "query.max-memory-per-node" - kind: - type: "file" - file: "config.properties" - datatype: - type: "string" - unit: *unitMemory - roles: - - name: "coordinator" - required: false - - name: "worker" - required: false - asOfVersion: "0.0.0" - - - property: &httpServerAuthenticationType - propertyNames: - - name: "http-server.authentication.type" - kind: - type: "file" - file: "config.properties" - datatype: - type: "string" - roles: - - name: "coordinator" - required: false - asOfVersion: "0.0.0" - -################################################################################################### -# jvm.config -################################################################################################### - -################################################################################################### -# log.properties -################################################################################################### - - - property: &ioTrino - propertyNames: - - name: "io.trino" - kind: - type: "file" - file: "log.properties" - datatype: - type: "string" - defaultValues: - - fromVersion: "0.0.0" - value: "INFO" - allowedValues: - - "INFO" - - "DEBUG" - - "WARN" - - "ERROR" - roles: - - name: "coordinator" - required: true - - name: "worker" - required: true - asOfVersion: "0.0.0" + units: [] +properties: [] diff --git a/docs/modules/trino/pages/reference/commandline-parameters.adoc b/docs/modules/trino/pages/reference/commandline-parameters.adoc index f4f4dd366..4e27f3283 100644 --- a/docs/modules/trino/pages/reference/commandline-parameters.adoc +++ b/docs/modules/trino/pages/reference/commandline-parameters.adoc @@ -2,19 +2,6 @@ This operator accepts the following command line parameters: -== product-config - -*Default value*: `/etc/stackable/trino-operator/config-spec/properties.yaml` - -*Required*: false - -*Multiple values:* false - -[source] ----- -cargo run -- run --product-config /foo/bar/properties.yaml ----- - == watch-namespace *Default value*: All namespaces diff --git a/docs/modules/trino/pages/reference/environment-variables.adoc b/docs/modules/trino/pages/reference/environment-variables.adoc index ae221056e..fddbd1ad0 100644 --- a/docs/modules/trino/pages/reference/environment-variables.adoc +++ b/docs/modules/trino/pages/reference/environment-variables.adoc @@ -33,32 +33,6 @@ docker run \ oci.stackable.tech/sdp/trino-operator:0.0.0-dev ---- -== PRODUCT_CONFIG - -*Default value*: `/etc/stackable/trino-operator/config-spec/properties.yaml` - -*Required*: false - -*Multiple values*: false - -[source] ----- -export PRODUCT_CONFIG=/foo/bar/properties.yaml -cargo run -- run ----- - -or via docker: - ----- -docker run \ - --name trino-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/trino-operator:0.0.0-dev ----- - == WATCH_NAMESPACE *Default value*: All namespaces diff --git a/extra/crds.yaml b/extra/crds.yaml index 59515338a..db68d3d08 100644 --- a/extra/crds.yaml +++ b/extra/crds.yaml @@ -354,6 +354,9 @@ spec: properties: configMap: description: Name of the [discovery ConfigMap](https://docs.stackable.tech/home/nightly/concepts/service_discovery) providing information about the HDFS cluster. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string required: - configMap @@ -638,6 +641,9 @@ spec: properties: configMap: description: Name of the [discovery ConfigMap](https://docs.stackable.tech/home/nightly/concepts/service_discovery) providing information about the HDFS cluster. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string required: - configMap @@ -898,7 +904,10 @@ spec: This setting controls: - Which cert the servers should use to authenticate themselves against other servers - Which ca.crt to use when validating the other server + 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 @@ -907,7 +916,10 @@ spec: This setting controls: - If TLS encryption is used at all - Which cert the servers should use to authenticate themselves against the client + 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: @@ -916,7 +928,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 required: - catalogLabelSelector @@ -1340,9 +1355,15 @@ spec: type: boolean type: object queryMaxMemory: + description: |- + This is the max amount of user memory a query can use across the entire cluster. + See nullable: true type: string queryMaxMemoryPerNode: + description: |- + This is the max amount of user memory a query can use on a worker. + See nullable: true type: string requestedSecretLifetime: @@ -1420,72 +1441,44 @@ spec: access-control.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 config.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 exchange-manager.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 log.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 node.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 spooling-manager.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: @@ -1551,6 +1544,9 @@ spec: listenerClass: default: cluster-internal description: This field controls which [ListenerClass](https://docs.stackable.tech/home/nightly/listener-operator/listenerclass.html) is used to expose the coordinator. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string podDisruptionBudget: default: @@ -1967,9 +1963,15 @@ spec: type: boolean type: object queryMaxMemory: + description: |- + This is the max amount of user memory a query can use across the entire cluster. + See nullable: true type: string queryMaxMemoryPerNode: + description: |- + This is the max amount of user memory a query can use on a worker. + See nullable: true type: string requestedSecretLifetime: @@ -2047,72 +2049,44 @@ spec: access-control.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 config.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 exchange-manager.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 log.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 node.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 spooling-manager.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: @@ -2646,9 +2620,15 @@ spec: type: boolean type: object queryMaxMemory: + description: |- + This is the max amount of user memory a query can use across the entire cluster. + See nullable: true type: string queryMaxMemoryPerNode: + description: |- + This is the max amount of user memory a query can use on a worker. + See nullable: true type: string requestedSecretLifetime: @@ -2726,72 +2706,44 @@ spec: access-control.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 config.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 exchange-manager.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 log.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 node.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 spooling-manager.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: @@ -3268,9 +3220,15 @@ spec: type: boolean type: object queryMaxMemory: + description: |- + This is the max amount of user memory a query can use across the entire cluster. + See nullable: true type: string queryMaxMemoryPerNode: + description: |- + This is the max amount of user memory a query can use on a worker. + See nullable: true type: string requestedSecretLifetime: @@ -3348,72 +3306,44 @@ spec: access-control.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 config.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 exchange-manager.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 log.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 node.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 spooling-manager.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: @@ -3605,6 +3535,9 @@ spec: properties: configMap: description: Name of the [discovery ConfigMap](https://docs.stackable.tech/home/nightly/concepts/service_discovery) providing information about the HDFS cluster. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string required: - configMap @@ -3614,6 +3547,9 @@ spec: properties: configMap: description: Name of the [discovery ConfigMap](https://docs.stackable.tech/home/nightly/concepts/service_discovery) providing information about the Hive metastore. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string required: - configMap @@ -3875,6 +3811,9 @@ spec: properties: configMap: description: Name of the [discovery ConfigMap](https://docs.stackable.tech/home/nightly/concepts/service_discovery) providing information about the HDFS cluster. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string required: - configMap @@ -3884,6 +3823,9 @@ spec: properties: configMap: description: Name of the [discovery ConfigMap](https://docs.stackable.tech/home/nightly/concepts/service_discovery) providing information about the Hive metastore. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string required: - configMap @@ -4054,6 +3996,9 @@ spec: properties: configMap: description: Name of the [discovery ConfigMap](https://docs.stackable.tech/home/nightly/concepts/service_discovery) providing information about the HDFS cluster. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string required: - configMap @@ -4068,6 +4013,9 @@ spec: properties: configMap: description: Name of the [discovery ConfigMap](https://docs.stackable.tech/home/nightly/concepts/service_discovery) providing information about the Hive metastore. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string required: - configMap diff --git a/rust/operator-binary/Cargo.toml b/rust/operator-binary/Cargo.toml index efa9d3472..62fa0c049 100644 --- a/rust/operator-binary/Cargo.toml +++ b/rust/operator-binary/Cargo.toml @@ -10,7 +10,6 @@ publish = false build = "build.rs" [dependencies] -product-config.workspace = true stackable-operator.workspace = true anyhow.workspace = true @@ -19,13 +18,13 @@ clap.workspace = true const_format.workspace = true futures.workspace = true indoc.workspace = true +serde_yaml.workspace = true +serde.workspace = true +serde_json.workspace = true snafu.workspace = true strum.workspace = true tokio.workspace = true tracing.workspace = true -serde_yaml.workspace = true -serde.workspace = true -serde_json.workspace = true [dev-dependencies] rstest.workspace = true diff --git a/rust/operator-binary/src/authentication/mod.rs b/rust/operator-binary/src/authentication/mod.rs index c31172429..69fde6544 100644 --- a/rust/operator-binary/src/authentication/mod.rs +++ b/rust/operator-binary/src/authentication/mod.rs @@ -19,6 +19,7 @@ use stackable_operator::{ crd::authentication::core, k8s_openapi::api::core::v1::{Container, EnvVar, Volume, VolumeMount}, kube::{ResourceExt, runtime::reflector::ObjectRef}, + v2::types::kubernetes::SecretName, }; use strum::EnumDiscriminants; use tracing::trace; @@ -50,11 +51,6 @@ pub enum Error { authentication_class: ObjectRef, }, - #[snafu(display("failed to format trino authentication java properties"))] - FailedToWriteJavaProperties { - source: product_config::writer::PropertiesWriterError, - }, - #[snafu(display("failed to configure trino password authentication"))] InvalidPasswordAuthenticationConfig { source: password::Error }, @@ -84,10 +80,13 @@ type Result = std::result::Result; /// may be in parts reused in other operators. #[derive(Clone, Debug, Default)] pub struct TrinoAuthenticationConfig { + /// The enabled `http-server.authentication.type` values, in evaluation order. Empty when no + /// authentication is configured. + authentication_types: Vec, /// All config properties that have to be added to the `config.properties` of the given role config_properties: HashMap>, /// All extra config files required for authentication for each role. - config_files: HashMap>, + config_files: HashMap>>, /// Additional env variables for a certain role and container env_vars: HashMap>>, /// All extra container commands for a certain role and container @@ -100,7 +99,7 @@ pub struct TrinoAuthenticationConfig { /// Additional side car container for the provided role sidecar_containers: HashMap>, /// Secrets which can be hot-reloaded and should be excluded from the restart controller - hot_reloaded_secrets: BTreeSet, + hot_reloaded_secrets: BTreeSet, } impl TrinoAuthenticationConfig { @@ -140,6 +139,7 @@ impl TrinoAuthenticationConfig { http_server_authentication_types.join(","), ); } + authentication_config.authentication_types = http_server_authentication_types; trace!( "Final Trino authentication config: {:?}", @@ -149,6 +149,11 @@ impl TrinoAuthenticationConfig { Ok(authentication_config) } + /// Whether no authentication is configured. + pub fn is_empty(&self) -> bool { + self.authentication_types.is_empty() + } + /// Automatically add volumes, volume mounts, commands and containers to /// the respective pod / container builders. pub fn add_authentication_pod_and_volume_config( @@ -211,13 +216,18 @@ impl TrinoAuthenticationConfig { .insert(property_name, property_value); } - /// Add config file for a given role. The file_content must already be formatted to its final - /// representation in the file. - pub fn add_config_file(&mut self, role: TrinoRole, file_name: String, file_content: String) { + /// Add config file for a given role. The `properties` are stored as a raw key/value map and + /// rendered to their final file representation by the ConfigMap builder when writing. + pub fn add_config_file( + &mut self, + role: TrinoRole, + file_name: String, + properties: BTreeMap, + ) { self.config_files .entry(role) .or_default() - .insert(file_name, file_content); + .insert(file_name, properties); } /// Add env variables for a given role and container. @@ -318,7 +328,7 @@ impl TrinoAuthenticationConfig { } /// Retrieve additional config files for a given role. - pub fn config_files(&self, role: &TrinoRole) -> BTreeMap { + pub fn config_files(&self, role: &TrinoRole) -> BTreeMap> { self.config_files.get(role).cloned().unwrap_or_default() } @@ -326,9 +336,7 @@ impl TrinoAuthenticationConfig { pub fn env_vars(&self, role: &TrinoRole, container: &crate::crd::Container) -> Vec { self.env_vars .get(role) - .cloned() - .unwrap_or_default() - .get(container) + .and_then(|by_container| by_container.get(container)) .cloned() .unwrap_or_default() } @@ -337,9 +345,7 @@ impl TrinoAuthenticationConfig { pub fn commands(&self, role: &TrinoRole, container: &crate::crd::Container) -> Vec { self.commands .get(role) - .cloned() - .unwrap_or_default() - .get(container) + .and_then(|by_container| by_container.get(container)) .cloned() .unwrap_or_default() } @@ -371,12 +377,12 @@ impl TrinoAuthenticationConfig { } /// Retrieve all Secrets which can be hot-reloaded - pub fn hot_reloaded_secrets(&self) -> &BTreeSet { + pub fn hot_reloaded_secrets(&self) -> &BTreeSet { &self.hot_reloaded_secrets } /// Add a Secret which can be hot-reloaded - pub fn add_hot_reloaded_secret(&mut self, secret_name: String) { + pub fn add_hot_reloaded_secret(&mut self, secret_name: SecretName) { self.hot_reloaded_secrets.insert(secret_name); } @@ -767,23 +773,73 @@ mod tests { let config_files = setup_authentication_config().config_files(&TrinoRole::Coordinator); assert_eq!( - config_files.get(&format!("{FILE_AUTH_CLASS_1}-password-file-auth.properties")), - Some(format!("file.password-file=/stackable/users/{FILE_AUTH_CLASS_1}.db\npassword-authenticator.name=file\n")).as_ref() - ); + config_files.get(&format!( + "{FILE_AUTH_CLASS_1}-password-file-auth.properties" + )), + Some(&BTreeMap::from([ + ( + "file.password-file".to_string(), + format!("/stackable/users/{FILE_AUTH_CLASS_1}.db") + ), + ( + "password-authenticator.name".to_string(), + "file".to_string() + ), + ])) + ); assert_eq!( - config_files.get(&format!("{FILE_AUTH_CLASS_2}-password-file-auth.properties")), - Some(format!("file.password-file=/stackable/users/{FILE_AUTH_CLASS_2}.db\npassword-authenticator.name=file\n")).as_ref() + config_files.get(&format!( + "{FILE_AUTH_CLASS_2}-password-file-auth.properties" + )), + Some(&BTreeMap::from([ + ( + "file.password-file".to_string(), + format!("/stackable/users/{FILE_AUTH_CLASS_2}.db") + ), + ( + "password-authenticator.name".to_string(), + "file".to_string() + ), + ])) ); assert_eq!( - config_files.get(&format!("{LDAP_AUTH_CLASS_1}-password-ldap-auth.properties")), - Some(format!("ldap.allow-insecure=true\nldap.group-auth-pattern=(&(uid\\=${{USER}}))\nldap.url=ldap\\://{HOST_NAME}\\:389\nldap.user-base-dn={SEARCH_BASE}\npassword-authenticator.name=ldap\n")).as_ref() + config_files.get(&format!( + "{LDAP_AUTH_CLASS_1}-password-ldap-auth.properties" + )), + Some(&BTreeMap::from([ + ("ldap.allow-insecure".to_string(), "true".to_string()), + ( + "ldap.group-auth-pattern".to_string(), + "(&(uid=${USER}))".to_string() + ), + ("ldap.url".to_string(), format!("ldap://{HOST_NAME}:389")), + ("ldap.user-base-dn".to_string(), SEARCH_BASE.to_string()), + ( + "password-authenticator.name".to_string(), + "ldap".to_string() + ), + ])) ); assert_eq!( - config_files.get(&format!("{LDAP_AUTH_CLASS_2}-password-ldap-auth.properties")), - Some(format!("ldap.allow-insecure=true\nldap.group-auth-pattern=(&(uid\\=${{USER}}))\nldap.url=ldap\\://{HOST_NAME}\\:389\nldap.user-base-dn={SEARCH_BASE}\npassword-authenticator.name=ldap\n")).as_ref() + config_files.get(&format!( + "{LDAP_AUTH_CLASS_2}-password-ldap-auth.properties" + )), + Some(&BTreeMap::from([ + ("ldap.allow-insecure".to_string(), "true".to_string()), + ( + "ldap.group-auth-pattern".to_string(), + "(&(uid=${USER}))".to_string() + ), + ("ldap.url".to_string(), format!("ldap://{HOST_NAME}:389")), + ("ldap.user-base-dn".to_string(), SEARCH_BASE.to_string()), + ( + "password-authenticator.name".to_string(), + "ldap".to_string() + ), + ])) ); } diff --git a/rust/operator-binary/src/authentication/oidc/mod.rs b/rust/operator-binary/src/authentication/oidc/mod.rs index 5f2449e6e..081534282 100644 --- a/rust/operator-binary/src/authentication/oidc/mod.rs +++ b/rust/operator-binary/src/authentication/oidc/mod.rs @@ -8,7 +8,7 @@ use stackable_operator::{ use crate::{ authentication::TrinoAuthenticationConfig, - command, + controller::build::command, crd::{STACKABLE_CLIENT_TLS_DIR, TrinoRole}, }; diff --git a/rust/operator-binary/src/authentication/password/file.rs b/rust/operator-binary/src/authentication/password/file.rs index 476e8cd74..650e951bc 100644 --- a/rust/operator-binary/src/authentication/password/file.rs +++ b/rust/operator-binary/src/authentication/password/file.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::{collections::BTreeMap, str::FromStr}; use snafu::{ResultExt, Snafu}; use stackable_operator::{ @@ -15,12 +15,16 @@ use stackable_operator::{ k8s_openapi::api::core::v1::{Container, Volume, VolumeMount}, product_logging::{self, spec::AutomaticContainerLogConfig}, utils::COMMON_BASH_TRAP_FUNCTIONS, + v2::types::kubernetes::{SecretName, VolumeName}, }; -use crate::{authentication::password::PASSWORD_AUTHENTICATOR_NAME, controller::STACKABLE_LOG_DIR}; +use crate::{ + authentication::password::PASSWORD_AUTHENTICATOR_NAME, + controller::build::resource::statefulset::LOG_VOLUME_NAME, trino_controller::STACKABLE_LOG_DIR, +}; // mounts -const PASSWORD_DB_VOLUME_NAME: &str = "users"; +stackable_operator::constant!(PASSWORD_DB_VOLUME_NAME: VolumeName = "users"); pub const PASSWORD_DB_VOLUME_MOUNT_PATH: &str = "/stackable/users"; pub const PASSWORD_AUTHENTICATOR_SECRET_MOUNT_PATH: &str = "/stackable/auth-secrets"; // trino properties @@ -33,6 +37,11 @@ pub enum Error { AddVolumeMounts { source: builder::pod::container::Error, }, + + #[snafu(display("failed to parse user credentials Secret name"))] + ParseSecretName { + source: stackable_operator::v2::macros::attributed_string_type::Error, + }, } #[derive(Clone, Debug)] @@ -66,15 +75,15 @@ impl FileAuthenticator { } /// Return the name of the Secret providing the usernames and passwords - pub fn secret_name(&self) -> String { - self.file.user_credentials_secret.name.clone() + pub fn secret_name(&self) -> Result { + SecretName::from_str(&self.file.user_credentials_secret.name).context(ParseSecretNameSnafu) } /// Build the volume for the user secret - pub fn secret_volume(&self) -> Volume { - VolumeBuilder::new(self.secret_volume_name()) - .with_secret(self.secret_name(), false) - .build() + pub fn secret_volume(&self) -> Result { + Ok(VolumeBuilder::new(self.secret_volume_name()) + .with_secret(self.secret_name()?, false) + .build()) } /// Build the volume mount for the user secret @@ -84,14 +93,14 @@ impl FileAuthenticator { /// Build the volume for the user password db pub fn password_db_volume() -> Volume { - VolumeBuilder::new(PASSWORD_DB_VOLUME_NAME) + VolumeBuilder::new(&*PASSWORD_DB_VOLUME_NAME) .with_empty_dir(None::, None) .build() } /// Build the volume mount for the user password db pub fn password_db_volume_mount() -> VolumeMount { - VolumeMountBuilder::new(PASSWORD_DB_VOLUME_NAME, PASSWORD_DB_VOLUME_MOUNT_PATH).build() + VolumeMountBuilder::new(&*PASSWORD_DB_VOLUME_NAME, PASSWORD_DB_VOLUME_MOUNT_PATH).build() } fn password_file_name(&self) -> String { @@ -187,7 +196,7 @@ wait_for_termination $! .add_volume_mounts(volume_mounts) .context(AddVolumeMountsSnafu)? // fixed - .add_volume_mount("log", STACKABLE_LOG_DIR) + .add_volume_mount(&*LOG_VOLUME_NAME, STACKABLE_LOG_DIR) .context(AddVolumeMountsSnafu)? .resources( ResourceRequirementsBuilder::new() diff --git a/rust/operator-binary/src/authentication/password/ldap.rs b/rust/operator-binary/src/authentication/password/ldap.rs index 671b4a100..a32262e8c 100644 --- a/rust/operator-binary/src/authentication/password/ldap.rs +++ b/rust/operator-binary/src/authentication/password/ldap.rs @@ -58,15 +58,12 @@ impl LdapAuthenticator { /// Return the content of the authenticator config file to register with Trino pub fn config_file_data(&self) -> Result, Error> { let mut config_data = BTreeMap::new(); - self.ldap.endpoint_url().context(LdapEndpointSnafu)?; + let endpoint_url = self.ldap.endpoint_url().context(LdapEndpointSnafu)?; config_data.insert( password::PASSWORD_AUTHENTICATOR_NAME.to_string(), PASSWORD_AUTHENTICATOR_NAME_LDAP.to_string(), ); - config_data.insert( - LDAP_URL.to_string(), - self.ldap.endpoint_url().context(LdapEndpointSnafu)?.into(), - ); + config_data.insert(LDAP_URL.to_string(), endpoint_url.into()); config_data.insert(LDAP_USER_BASE_DN.to_string(), self.ldap.search_base.clone()); diff --git a/rust/operator-binary/src/authentication/password/mod.rs b/rust/operator-binary/src/authentication/password/mod.rs index f5eeb09ce..08e8c5e95 100644 --- a/rust/operator-binary/src/authentication/password/mod.rs +++ b/rust/operator-binary/src/authentication/password/mod.rs @@ -8,8 +8,6 @@ //! - volume and volume mounts //! - extra containers and commands //! -use std::collections::BTreeMap; - use snafu::{ResultExt, Snafu}; use stackable_operator::commons::product_image_selection::ResolvedProductImage; use tracing::trace; @@ -34,16 +32,14 @@ pub enum Error { #[snafu(display("failed to configure LDAP password authentication"))] InvalidLdapAuthenticationConfiguration { source: ldap::Error }, - #[snafu(display("failed to write password authentication config file"))] - WritePasswordAuthenticationFile { - source: product_config::writer::PropertiesWriterError, - }, - #[snafu(display("failed to create LDAP Volumes and VolumeMounts"))] LdapVolumeAndVolumeMounts { source: ldap::Error }, #[snafu(display("failed to create LDAP Volumes and VolumeMounts"))] BuildPasswordFileUpdateContainer { source: file::Error }, + + #[snafu(display("failed to resolve password file authenticator Secret"))] + FileAuthenticatorSecret { source: file::Error }, } #[derive(Clone, Debug, Default)] @@ -93,18 +89,14 @@ impl TrinoPasswordAuthentication { password_authentication_config.add_config_file( TrinoRole::Coordinator, config_file_name, - product_config::writer::to_java_properties_string( - file_authenticator - .config_file_data() - .into_iter() - .map(|(k, v)| (k, Some(v))) - .collect::>>() - .iter(), - ) - .context(WritePasswordAuthenticationFileSnafu)?, + file_authenticator.config_file_data(), ); // required volumes - password_authentication_config.add_volume(file_authenticator.secret_volume()); + password_authentication_config.add_volume( + file_authenticator + .secret_volume() + .context(FileAuthenticatorSecretSnafu)?, + ); password_authentication_config .add_volume(FileAuthenticator::password_db_volume()); @@ -120,8 +112,11 @@ impl TrinoPasswordAuthentication { FileAuthenticator::password_db_volume_mount(), ); - password_authentication_config - .add_hot_reloaded_secret(file_authenticator.secret_name()); + password_authentication_config.add_hot_reloaded_secret( + file_authenticator + .secret_name() + .context(FileAuthenticatorSecretSnafu)?, + ); } TrinoPasswordAuthenticator::Ldap(ldap_authenticator) => { let config_file_name = ldap_authenticator.config_file_name(); @@ -133,16 +128,9 @@ impl TrinoPasswordAuthentication { password_authentication_config.add_config_file( TrinoRole::Coordinator, config_file_name, - product_config::writer::to_java_properties_string( - ldap_authenticator - .config_file_data() - .context(InvalidLdapAuthenticationConfigurationSnafu)? - .into_iter() - .map(|(k, v)| (k, Some(v))) - .collect::>>() - .iter(), - ) - .context(WritePasswordAuthenticationFileSnafu)?, + ldap_authenticator + .config_file_data() + .context(InvalidLdapAuthenticationConfigurationSnafu)?, ); // extra commands @@ -206,6 +194,8 @@ impl TrinoPasswordAuthentication { #[cfg(test)] mod tests { + use std::collections::BTreeMap; + use stackable_operator::crd::authentication::{ldap, r#static}; use super::*; @@ -300,16 +290,24 @@ mod tests { // check file auth assert_eq!( config_files.get(&file_auth_1.config_file_name()).unwrap(), - &format!( - "file.password-file=/stackable/users/{FILE_AUTH_CLASS_1}.db\npassword-authenticator.name=file\n" - ) + &BTreeMap::from([ + ( + "file.password-file".to_string(), + format!("/stackable/users/{FILE_AUTH_CLASS_1}.db") + ), + ( + "password-authenticator.name".to_string(), + "file".to_string() + ), + ]) ); // check ldap - assert!( + assert_eq!( config_files .get(&ldap_auth_1.config_file_name()) .unwrap() - .contains("password-authenticator.name=ldap") + .get("password-authenticator.name"), + Some(&"ldap".to_string()) ); // Coordinator diff --git a/rust/operator-binary/src/authorization/opa.rs b/rust/operator-binary/src/authorization/opa.rs index 1fa9bd95f..eb758713c 100644 --- a/rust/operator-binary/src/authorization/opa.rs +++ b/rust/operator-binary/src/authorization/opa.rs @@ -1,14 +1,17 @@ -use std::collections::BTreeMap; +use std::{collections::BTreeMap, str::FromStr}; use stackable_operator::{ - client::Client, commons::opa::OpaApiVersion, k8s_openapi::api::core::v1::ConfigMap, - kube::ResourceExt, + client::Client, + commons::opa::OpaApiVersion, + k8s_openapi::api::core::v1::ConfigMap, + v2::types::kubernetes::{NamespaceName, SecretClassName, VolumeName}, }; use crate::crd::v1alpha1; -pub const OPA_TLS_VOLUME_NAME: &str = "opa-tls"; +stackable_operator::constant!(pub OPA_TLS_VOLUME_NAME: VolumeName = "opa-tls"); +#[derive(Clone, Debug)] pub struct TrinoOpaConfig { /// URI for OPA policies, e.g. /// `http://localhost:8081/v1/data/trino/allow` @@ -33,13 +36,14 @@ pub struct TrinoOpaConfig { /// Optional TLS secret class for OPA communication. /// If set, the CA certificate from this secret class will be added /// to Trino's truststore to make it trust OPA's TLS certificate. - pub(crate) tls_secret_class: Option, + pub(crate) tls_secret_class: Option, } impl TrinoOpaConfig { pub async fn from_opa_config( client: &Client, trino: &v1alpha1::TrinoCluster, + namespace: &NamespaceName, opa_config: &v1alpha1::TrinoAuthorizationOpaConfig, ) -> Result { let non_batched_connection_string = opa_config @@ -85,14 +89,12 @@ impl TrinoOpaConfig { }; let tls_secret_class = client - .get::( - &opa_config.opa.config_map_name, - trino.namespace().as_deref().unwrap_or("default"), - ) + .get::(&opa_config.opa.config_map_name, namespace.as_ref()) .await .ok() .and_then(|cm| cm.data) - .and_then(|mut data| data.remove("OPA_SECRET_CLASS")); + .and_then(|mut data| data.remove("OPA_SECRET_CLASS")) + .and_then(|secret_class| SecretClassName::from_str(&secret_class).ok()); Ok(TrinoOpaConfig { non_batched_connection_string, @@ -104,22 +106,22 @@ impl TrinoOpaConfig { }) } - pub fn as_config(&self) -> BTreeMap> { + pub fn as_config(&self) -> BTreeMap { let mut config = BTreeMap::from([ - ("access-control.name".to_string(), Some("opa".to_string())), + ("access-control.name".to_string(), "opa".to_string()), ( "opa.policy.uri".to_string(), - Some(self.non_batched_connection_string.clone()), + self.non_batched_connection_string.clone(), ), ( "opa.policy.batched-uri".to_string(), - Some(self.batched_connection_string.clone()), + self.batched_connection_string.clone(), ), ]); if let Some(row_filters_connection_string) = &self.row_filters_connection_string { config.insert( "opa.policy.row-filters-uri".to_string(), - Some(row_filters_connection_string.clone()), + row_filters_connection_string.clone(), ); } if let Some(batched_column_masking_connection_string) = @@ -127,21 +129,91 @@ impl TrinoOpaConfig { { config.insert( "opa.policy.batch-column-masking-uri".to_string(), - Some(batched_column_masking_connection_string.clone()), + batched_column_masking_connection_string.clone(), ); } if self.allow_permission_management_operations { config.insert( "opa.allow-permission-management-operations".to_string(), - Some("true".to_string()), + "true".to_string(), ); } config } pub fn tls_mount_path(&self) -> Option { - self.tls_secret_class - .as_ref() - .map(|_| format!("/stackable/secrets/{OPA_TLS_VOLUME_NAME}")) + self.tls_secret_class.as_ref().map(|_| { + format!( + "/stackable/secrets/{opa_tls_volume_name}", + opa_tls_volume_name = &*OPA_TLS_VOLUME_NAME + ) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn minimal_opa() -> TrinoOpaConfig { + TrinoOpaConfig { + non_batched_connection_string: "http://opa/allow".to_string(), + batched_connection_string: "http://opa/batch".to_string(), + row_filters_connection_string: None, + batched_column_masking_connection_string: None, + allow_permission_management_operations: false, + tls_secret_class: None, + } + } + + #[test] + fn as_config_renders_only_required_keys_when_optionals_are_unset() { + let config = minimal_opa().as_config(); + + assert_eq!( + config.get("access-control.name").map(String::as_str), + Some("opa") + ); + assert_eq!( + config.get("opa.policy.uri").map(String::as_str), + Some("http://opa/allow") + ); + assert_eq!( + config.get("opa.policy.batched-uri").map(String::as_str), + Some("http://opa/batch") + ); + assert!(!config.contains_key("opa.policy.row-filters-uri")); + assert!(!config.contains_key("opa.policy.batch-column-masking-uri")); + assert!(!config.contains_key("opa.allow-permission-management-operations")); + } + + #[test] + fn as_config_renders_optional_keys_when_set() { + let config = TrinoOpaConfig { + row_filters_connection_string: Some("http://opa/rowFilters".to_string()), + batched_column_masking_connection_string: Some( + "http://opa/batchColumnMasks".to_string(), + ), + allow_permission_management_operations: true, + ..minimal_opa() + } + .as_config(); + + assert_eq!( + config.get("opa.policy.row-filters-uri").map(String::as_str), + Some("http://opa/rowFilters") + ); + assert_eq!( + config + .get("opa.policy.batch-column-masking-uri") + .map(String::as_str), + Some("http://opa/batchColumnMasks") + ); + assert_eq!( + config + .get("opa.allow-permission-management-operations") + .map(String::as_str), + Some("true") + ); } } diff --git a/rust/operator-binary/src/catalog/black_hole.rs b/rust/operator-binary/src/catalog/black_hole.rs index 9377351de..82ab529b5 100644 --- a/rust/operator-binary/src/catalog/black_hole.rs +++ b/rust/operator-binary/src/catalog/black_hole.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use stackable_operator::client::Client; +use stackable_operator::{client::Client, v2::types::kubernetes::NamespaceName}; use super::{FromTrinoCatalogError, ToCatalogConfig, config::CatalogConfig}; use crate::crd::catalog::black_hole::BlackHoleConnector; @@ -11,7 +11,7 @@ impl ToCatalogConfig for BlackHoleConnector { async fn to_catalog_config( &self, catalog_name: &str, - _catalog_namespace: Option, + _catalog_namespace: &NamespaceName, _client: &Client, _trino_version: u16, ) -> Result { diff --git a/rust/operator-binary/src/catalog/commons.rs b/rust/operator-binary/src/catalog/commons.rs index fc3f4e265..2558214e5 100644 --- a/rust/operator-binary/src/catalog/commons.rs +++ b/rust/operator-binary/src/catalog/commons.rs @@ -3,9 +3,9 @@ use snafu::{OptionExt, ResultExt, ensure}; use stackable_operator::{ builder::pod::volume::{VolumeBuilder, VolumeMountBuilder}, client::Client, - commons::tls_verification::{CaCert, TlsServerVerification, TlsVerification}, crd::s3, k8s_openapi::api::core::v1::ConfigMap, + v2::types::kubernetes::NamespaceName, }; use super::{ @@ -14,13 +14,13 @@ use super::{ from_trino_catalog_error::{ ConfigureS3Snafu, FailedToGetDiscoveryConfigMapDataKeySnafu, FailedToGetDiscoveryConfigMapDataSnafu, FailedToGetDiscoveryConfigMapSnafu, - ObjectHasNoNamespaceSnafu, S3TlsNoVerificationNotSupportedSnafu, S3TlsRequiredSnafu, + S3TlsNoVerificationNotSupportedSnafu, S3TlsRequiredSnafu, }, }; use crate::{ - command, + config, crd::{ - CONFIG_DIR_NAME, STACKABLE_CLIENT_TLS_DIR, + CONFIG_DIR_NAME, catalog::commons::{HdfsConnection, MetastoreConnection}, }, }; @@ -31,17 +31,12 @@ impl ExtendCatalogConfig for MetastoreConnection { &self, catalog_config: &mut CatalogConfig, catalog_name: &str, - catalog_namespace: Option, + catalog_namespace: &NamespaceName, client: &Client, _trino_version: u16, ) -> Result<(), FromTrinoCatalogError> { let hive_cm: ConfigMap = client - .get( - &self.config_map, - catalog_namespace - .as_deref() - .context(ObjectHasNoNamespaceSnafu)?, - ) + .get(self.config_map.as_ref(), catalog_namespace.as_ref()) .await .with_context(|_| FailedToGetDiscoveryConfigMapSnafu { catalog: catalog_name.to_string(), @@ -78,18 +73,13 @@ impl ExtendCatalogConfig for s3::v1alpha1::InlineConnectionOrReference { &self, catalog_config: &mut CatalogConfig, _catalog_name: &str, - catalog_namespace: Option, + catalog_namespace: &NamespaceName, client: &Client, trino_version: u16, ) -> Result<(), FromTrinoCatalogError> { let s3 = self .clone() - .resolve( - client, - catalog_namespace - .as_deref() - .context(ObjectHasNoNamespaceSnafu)?, - ) + .resolve(client, catalog_namespace.as_ref()) .await .context(ConfigureS3Snafu)?; @@ -140,23 +130,10 @@ impl ExtendCatalogConfig for s3::v1alpha1::InlineConnectionOrReference { 469.. => ensure!(s3.tls.uses_tls(), S3TlsRequiredSnafu), }; - if let Some(tls) = s3.tls.tls.as_ref() { - match &tls.verification { - TlsVerification::None {} => return S3TlsNoVerificationNotSupportedSnafu.fail(), - TlsVerification::Server(TlsServerVerification { - ca_cert: CaCert::WebPki {}, - }) => {} - TlsVerification::Server(TlsServerVerification { - ca_cert: CaCert::SecretClass(_), - }) => { - if let Some(ca_cert) = s3.tls.tls_ca_cert_mount_path() { - catalog_config.init_container_extra_start_commands.extend( - command::add_cert_to_truststore(&ca_cert, STACKABLE_CLIENT_TLS_DIR), - ); - } - } - } - } + catalog_config.init_container_extra_start_commands.extend( + config::s3::s3_tls_truststore_commands(&s3.tls) + .map_err(|_| S3TlsNoVerificationNotSupportedSnafu.build())?, + ); Ok(()) } @@ -168,7 +145,7 @@ impl ExtendCatalogConfig for HdfsConnection { &self, catalog_config: &mut CatalogConfig, catalog_name: &str, - _catalog_namespace: Option, + _catalog_namespace: &NamespaceName, _client: &Client, _trino_version: u16, ) -> Result<(), FromTrinoCatalogError> { diff --git a/rust/operator-binary/src/catalog/config.rs b/rust/operator-binary/src/catalog/config.rs index 8d4ef5530..86c9c42b7 100644 --- a/rust/operator-binary/src/catalog/config.rs +++ b/rust/operator-binary/src/catalog/config.rs @@ -5,12 +5,14 @@ use stackable_operator::{ k8s_openapi::api::core::v1::{ ConfigMapKeySelector, EnvVar, EnvVarSource, SecretKeySelector, Volume, VolumeMount, }, - kube::{Resource, ResourceExt}, + kube::Resource, + v2::types::kubernetes::NamespaceName, }; use super::{FromTrinoCatalogError, ToCatalogConfig}; use crate::crd::catalog::{TrinoCatalogConnector, v1alpha1}; +#[derive(Clone, Debug)] pub struct CatalogConfig { /// Name of the catalog pub name: String, @@ -105,6 +107,7 @@ impl CatalogConfig { pub async fn from_catalog( catalog: &v1alpha1::TrinoCatalog, client: &Client, + catalog_namespace: &NamespaceName, trino_version: u16, ) -> Result { let catalog_name = catalog @@ -112,7 +115,6 @@ impl CatalogConfig { .name .clone() .ok_or(FromTrinoCatalogError::InvalidCatalogSpec)?; - let catalog_namespace = catalog.namespace(); let to_catalog_config: &dyn ToCatalogConfig = match &catalog.spec.connector { TrinoCatalogConnector::BlackHole(black_hole_connector) => black_hole_connector, diff --git a/rust/operator-binary/src/catalog/delta_lake.rs b/rust/operator-binary/src/catalog/delta_lake.rs index 30380db3c..a2c089e90 100644 --- a/rust/operator-binary/src/catalog/delta_lake.rs +++ b/rust/operator-binary/src/catalog/delta_lake.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use stackable_operator::client::Client; +use stackable_operator::{client::Client, v2::types::kubernetes::NamespaceName}; use super::{ExtendCatalogConfig, FromTrinoCatalogError, ToCatalogConfig, config::CatalogConfig}; use crate::crd::catalog::delta_lake::DeltaLakeConnector; @@ -11,7 +11,7 @@ impl ToCatalogConfig for DeltaLakeConnector { async fn to_catalog_config( &self, catalog_name: &str, - catalog_namespace: Option, + catalog_namespace: &NamespaceName, client: &Client, trino_version: u16, ) -> Result { @@ -28,7 +28,7 @@ impl ToCatalogConfig for DeltaLakeConnector { .extend_catalog_config( &mut config, catalog_name, - catalog_namespace.clone(), + catalog_namespace, client, trino_version, ) @@ -38,7 +38,7 @@ impl ToCatalogConfig for DeltaLakeConnector { s3.extend_catalog_config( &mut config, catalog_name, - catalog_namespace.clone(), + catalog_namespace, client, trino_version, ) @@ -49,7 +49,7 @@ impl ToCatalogConfig for DeltaLakeConnector { hdfs.extend_catalog_config( &mut config, catalog_name, - catalog_namespace.clone(), + catalog_namespace, client, trino_version, ) diff --git a/rust/operator-binary/src/catalog/generic.rs b/rust/operator-binary/src/catalog/generic.rs index b7c42a86f..af3de842a 100644 --- a/rust/operator-binary/src/catalog/generic.rs +++ b/rust/operator-binary/src/catalog/generic.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use stackable_operator::client::Client; +use stackable_operator::{client::Client, v2::types::kubernetes::NamespaceName}; use super::{FromTrinoCatalogError, ToCatalogConfig, config::CatalogConfig}; use crate::crd::catalog::generic::{GenericConnector, Property}; @@ -9,7 +9,7 @@ impl ToCatalogConfig for GenericConnector { async fn to_catalog_config( &self, catalog_name: &str, - _catalog_namespace: Option, + _catalog_namespace: &NamespaceName, _client: &Client, _trino_version: u16, ) -> Result { diff --git a/rust/operator-binary/src/catalog/google_sheet.rs b/rust/operator-binary/src/catalog/google_sheet.rs index 7d85cc9b5..90396be22 100644 --- a/rust/operator-binary/src/catalog/google_sheet.rs +++ b/rust/operator-binary/src/catalog/google_sheet.rs @@ -2,6 +2,7 @@ use async_trait::async_trait; use stackable_operator::{ builder::pod::volume::{VolumeBuilder, VolumeMountBuilder}, client::Client, + v2::types::kubernetes::NamespaceName, }; use super::{FromTrinoCatalogError, ToCatalogConfig, config::CatalogConfig}; @@ -14,7 +15,7 @@ impl ToCatalogConfig for GoogleSheetConnector { async fn to_catalog_config( &self, catalog_name: &str, - _catalog_namespace: Option, + _catalog_namespace: &NamespaceName, _client: &Client, _trino_version: u16, ) -> Result { diff --git a/rust/operator-binary/src/catalog/hive.rs b/rust/operator-binary/src/catalog/hive.rs index aa7e72656..96ffdf19e 100644 --- a/rust/operator-binary/src/catalog/hive.rs +++ b/rust/operator-binary/src/catalog/hive.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use stackable_operator::client::Client; +use stackable_operator::{client::Client, v2::types::kubernetes::NamespaceName}; use super::{ExtendCatalogConfig, FromTrinoCatalogError, ToCatalogConfig, config::CatalogConfig}; use crate::crd::catalog::hive::HiveConnector; @@ -11,7 +11,7 @@ impl ToCatalogConfig for HiveConnector { async fn to_catalog_config( &self, catalog_name: &str, - catalog_namespace: Option, + catalog_namespace: &NamespaceName, client: &Client, trino_version: u16, ) -> Result { @@ -28,7 +28,7 @@ impl ToCatalogConfig for HiveConnector { .extend_catalog_config( &mut config, catalog_name, - catalog_namespace.clone(), + catalog_namespace, client, trino_version, ) @@ -38,7 +38,7 @@ impl ToCatalogConfig for HiveConnector { s3.extend_catalog_config( &mut config, catalog_name, - catalog_namespace.clone(), + catalog_namespace, client, trino_version, ) @@ -49,7 +49,7 @@ impl ToCatalogConfig for HiveConnector { hdfs.extend_catalog_config( &mut config, catalog_name, - catalog_namespace.clone(), + catalog_namespace, client, trino_version, ) diff --git a/rust/operator-binary/src/catalog/iceberg.rs b/rust/operator-binary/src/catalog/iceberg.rs index 4ab4498a9..65b6f41f3 100644 --- a/rust/operator-binary/src/catalog/iceberg.rs +++ b/rust/operator-binary/src/catalog/iceberg.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use stackable_operator::client::Client; +use stackable_operator::{client::Client, v2::types::kubernetes::NamespaceName}; use super::{ExtendCatalogConfig, FromTrinoCatalogError, ToCatalogConfig, config::CatalogConfig}; use crate::crd::catalog::iceberg::IcebergConnector; @@ -11,7 +11,7 @@ impl ToCatalogConfig for IcebergConnector { async fn to_catalog_config( &self, catalog_name: &str, - catalog_namespace: Option, + catalog_namespace: &NamespaceName, client: &Client, trino_version: u16, ) -> Result { @@ -29,7 +29,7 @@ impl ToCatalogConfig for IcebergConnector { .extend_catalog_config( &mut config, catalog_name, - catalog_namespace.clone(), + catalog_namespace, client, trino_version, ) @@ -40,7 +40,7 @@ impl ToCatalogConfig for IcebergConnector { s3.extend_catalog_config( &mut config, catalog_name, - catalog_namespace.clone(), + catalog_namespace, client, trino_version, ) @@ -51,7 +51,7 @@ impl ToCatalogConfig for IcebergConnector { hdfs.extend_catalog_config( &mut config, catalog_name, - catalog_namespace.clone(), + catalog_namespace, client, trino_version, ) diff --git a/rust/operator-binary/src/catalog/mod.rs b/rust/operator-binary/src/catalog/mod.rs index 9abc549a6..6a2f8ae38 100644 --- a/rust/operator-binary/src/catalog/mod.rs +++ b/rust/operator-binary/src/catalog/mod.rs @@ -12,24 +12,18 @@ pub mod tpch; use async_trait::async_trait; use snafu::Snafu; -use stackable_operator::{client::Client, commons::tls_verification::TlsClientDetailsError}; +use stackable_operator::{client::Client, v2::types::kubernetes::NamespaceName}; use self::config::CatalogConfig; #[derive(Debug, Snafu)] #[snafu(module)] pub enum FromTrinoCatalogError { - #[snafu(display("object has no namespace"))] - ObjectHasNoNamespace, - #[snafu(display("failed to configure S3 connection"))] ConfigureS3 { source: stackable_operator::crd::s3::v1alpha1::ConnectionError, }, - #[snafu(display("failed to configure S3 TLS client details"))] - ConfigureS3TlsClientDetails { source: TlsClientDetailsError }, - #[snafu(display("trino does not support disabling the TLS verification of S3 servers"))] S3TlsNoVerificationNotSupported, @@ -58,11 +52,6 @@ pub enum FromTrinoCatalogError { data_key: String, }, - #[snafu(display("failed to create the Secret Volume for the S3 credentials"))] - CreateS3CredentialsSecretOperatorVolume { - source: stackable_operator::builder::pod::volume::SecretOperatorVolumeSourceBuilderError, - }, - #[snafu(display("failed to get PostgreSQL connection details"))] GetPostgresConnectionDetails { source: stackable_operator::database_connections::Error, @@ -74,7 +63,7 @@ pub trait ToCatalogConfig { async fn to_catalog_config( &self, catalog_name: &str, - catalog_namespace: Option, + catalog_namespace: &NamespaceName, client: &Client, trino_version: u16, ) -> Result; @@ -86,7 +75,7 @@ pub trait ExtendCatalogConfig { &self, catalog_config: &mut CatalogConfig, catalog_name: &str, - catalog_namespace: Option, + catalog_namespace: &NamespaceName, client: &Client, trino_version: u16, ) -> Result<(), FromTrinoCatalogError>; diff --git a/rust/operator-binary/src/catalog/postgresql.rs b/rust/operator-binary/src/catalog/postgresql.rs index e5731641b..65ef335fb 100644 --- a/rust/operator-binary/src/catalog/postgresql.rs +++ b/rust/operator-binary/src/catalog/postgresql.rs @@ -2,6 +2,7 @@ use async_trait::async_trait; use snafu::ResultExt; use stackable_operator::{ client::Client, database_connections::drivers::jdbc::JdbcDatabaseConnection, + v2::types::kubernetes::NamespaceName, }; use super::{FromTrinoCatalogError, ToCatalogConfig, config::CatalogConfig}; @@ -17,7 +18,7 @@ impl ToCatalogConfig for PostgresqlConnector { async fn to_catalog_config( &self, catalog_name: &str, - _catalog_namespace: Option, + _catalog_namespace: &NamespaceName, _client: &Client, _trino_version: u16, ) -> Result { diff --git a/rust/operator-binary/src/catalog/tpcds.rs b/rust/operator-binary/src/catalog/tpcds.rs index 8eb65512c..2da5ceb7e 100644 --- a/rust/operator-binary/src/catalog/tpcds.rs +++ b/rust/operator-binary/src/catalog/tpcds.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use stackable_operator::client::Client; +use stackable_operator::{client::Client, v2::types::kubernetes::NamespaceName}; use super::{FromTrinoCatalogError, ToCatalogConfig, config::CatalogConfig}; use crate::crd::catalog::tpcds::TpcdsConnector; @@ -11,7 +11,7 @@ impl ToCatalogConfig for TpcdsConnector { async fn to_catalog_config( &self, catalog_name: &str, - _catalog_namespace: Option, + _catalog_namespace: &NamespaceName, _client: &Client, _trino_version: u16, ) -> Result { diff --git a/rust/operator-binary/src/catalog/tpch.rs b/rust/operator-binary/src/catalog/tpch.rs index dadc07a6c..e355de3cd 100644 --- a/rust/operator-binary/src/catalog/tpch.rs +++ b/rust/operator-binary/src/catalog/tpch.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use stackable_operator::client::Client; +use stackable_operator::{client::Client, v2::types::kubernetes::NamespaceName}; use super::{FromTrinoCatalogError, ToCatalogConfig, config::CatalogConfig}; use crate::crd::catalog::tpch::TpchConnector; @@ -11,7 +11,7 @@ impl ToCatalogConfig for TpchConnector { async fn to_catalog_config( &self, catalog_name: &str, - _catalog_namespace: Option, + _catalog_namespace: &NamespaceName, _client: &Client, _trino_version: u16, ) -> Result { diff --git a/rust/operator-binary/src/config/client_protocol.rs b/rust/operator-binary/src/config/client_protocol.rs index 96354c21a..472e71233 100644 --- a/rust/operator-binary/src/config/client_protocol.rs +++ b/rust/operator-binary/src/config/client_protocol.rs @@ -1,4 +1,4 @@ -// Consolidate Trino S3 properties in a single reusable struct. +//! Resolves the Trino client (spooling) protocol configuration. use std::collections::BTreeMap; @@ -20,17 +20,9 @@ use crate::{ pub enum Error { #[snafu(display("failed to resolve S3 connection"))] ResolveS3Connection { source: config::s3::Error }, - - #[snafu(display("trino does not support disabling the TLS verification of S3 servers"))] - S3TlsNoVerificationNotSupported, - - #[snafu(display("failed to convert data size for [{field}] to bytes"))] - QuantityConversion { - source: stackable_operator::memory::Error, - field: &'static str, - }, } +#[derive(Clone, Debug)] pub struct ResolvedClientProtocolConfig { /// Properties to add to config.properties pub config_properties: BTreeMap, diff --git a/rust/operator-binary/src/config/fault_tolerant_execution.rs b/rust/operator-binary/src/config/fault_tolerant_execution.rs index 852f15c3e..92bf7626a 100644 --- a/rust/operator-binary/src/config/fault_tolerant_execution.rs +++ b/rust/operator-binary/src/config/fault_tolerant_execution.rs @@ -21,6 +21,9 @@ use crate::{ }, }; +/// Sub-directory of [`CONFIG_DIR_NAME`] holding the HDFS exchange config. +const EXCHANGE_HDFS_CONFIG: &str = "exchange-hdfs-config"; + #[derive(Snafu, Debug)] pub enum Error { #[snafu(display("failed to resolve S3 connection"))] @@ -31,9 +34,6 @@ pub enum Error { #[snafu(display("failed to resolve S3 connection"))] ResolveS3Connection { source: config::s3::Error }, - #[snafu(display("trino does not support disabling the TLS verification of S3 servers"))] - S3TlsNoVerificationNotSupported, - #[snafu(display("failed to convert data size for [{field}] to bytes"))] QuantityConversion { source: stackable_operator::memory::Error, @@ -42,6 +42,7 @@ pub enum Error { } /// Fault tolerant execution configuration with external resources resolved +#[derive(Clone, Debug)] pub struct ResolvedFaultTolerantExecutionConfig { /// Properties to add to config.properties pub config_properties: BTreeMap, @@ -242,7 +243,7 @@ impl ResolvedFaultTolerantExecutionConfig { hdfs_config.skip_directory_scheme_validation, ); - let hdfs_config_dir = format!("{CONFIG_DIR_NAME}/exchange-hdfs-config"); + let hdfs_config_dir = format!("{CONFIG_DIR_NAME}/{EXCHANGE_HDFS_CONFIG}"); exchange_manager_properties.insert( "hdfs.config.resources".to_string(), format!("{hdfs_config_dir}/core-site.xml,{hdfs_config_dir}/hdfs-site.xml"), @@ -310,8 +311,8 @@ impl ResolvedFaultTolerantExecutionConfig { } fn resolve_hdfs_backend(&mut self, hdfs_config: &HdfsExchangeConfig) { - let hdfs_config_dir = format!("{CONFIG_DIR_NAME}/exchange-hdfs-config"); - let volume_name = "exchange-hdfs-config".to_string(); + let hdfs_config_dir = format!("{CONFIG_DIR_NAME}/{EXCHANGE_HDFS_CONFIG}"); + let volume_name = EXCHANGE_HDFS_CONFIG.to_string(); self.volumes.push( VolumeBuilder::new(&volume_name) diff --git a/rust/operator-binary/src/config/jvm.rs b/rust/operator-binary/src/config/jvm.rs index 0fa18c674..8b2baccae 100644 --- a/rust/operator-binary/src/config/jvm.rs +++ b/rust/operator-binary/src/config/jvm.rs @@ -3,14 +3,20 @@ use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ memory::{BinaryMultiple, MemoryQuantity}, - role_utils::{self, JvmArgumentOverrides}, + v2::jvm_argument_overrides::JvmArgumentOverrides, }; -use crate::crd::{ - JVM_HEAP_FACTOR, JVM_SECURITY_PROPERTIES, METRICS_PORT, RW_CONFIG_DIR_NAME, - STACKABLE_CLIENT_TLS_DIR, STACKABLE_TLS_STORE_PASSWORD, TrinoRoleType, v1alpha1, +use crate::{ + controller::ValidatedTrinoConfig, + crd::{ + METRICS_PORT, RW_CONFIG_DIR_NAME, STACKABLE_CLIENT_TLS_DIR, STACKABLE_TLS_STORE_PASSWORD, + }, }; +const JVM_SECURITY_PROPERTIES: &str = "security.properties"; + +const JVM_HEAP_FACTOR: f32 = 0.8; + #[derive(Snafu, Debug)] pub enum Error { #[snafu(display("failed to convert java heap config to unit [{unit}]"))] @@ -32,18 +38,14 @@ pub enum Error { "Trino version {version} is not supported. Only specific versions are handled due to version specific JVM configuration generation" ))] TrinoVersionNotSupported { version: u16 }, - - #[snafu(display("failed to merge jvm argument overrides"))] - MergeJvmArgumentOverrides { source: role_utils::Error }, } // Currently works for all supported versions (as of 2024-09-04) but maybe be changed // in the future depending on the role and version. pub fn jvm_config( product_version: u16, - merged_config: &v1alpha1::TrinoConfig, - role: &TrinoRoleType, - role_group: &str, + merged_config: &ValidatedTrinoConfig, + jvm_argument_overrides: &JvmArgumentOverrides, ) -> Result { let memory_unit = BinaryMultiple::Mebi; let heap_size = MemoryQuantity::try_from( @@ -88,14 +90,10 @@ pub fn jvm_config( jvm_args.push("# Arguments from jvmArgumentOverrides".to_owned()); - let operator_generated = JvmArgumentOverrides::new_with_only_additions(jvm_args); - let merged_jvm_argument_overrides = role - .get_merged_jvm_argument_overrides(role_group, &operator_generated) - .context(MergeJvmArgumentOverridesSnafu)?; - - Ok(merged_jvm_argument_overrides - .effective_jvm_config_after_merging() - .join("\n")) + // `jvm_argument_overrides` already carries the merged role + role-group overrides (merged by + // `with_validated_config` in the validate step). Applying them to the operator-generated args + // layers the overrides on top, in the order: operator-generated <- role <- role group. + Ok(jvm_argument_overrides.apply_to(jvm_args).join("\n")) } /// For tests we don't actually look at the Trino version, and return a single "representative" @@ -255,18 +253,22 @@ mod tests { let trino: v1alpha1::TrinoCluster = serde_yaml::from_str(trino_cluster).expect("illegal test input"); - let role = TrinoRole::Coordinator; - let rolegroup_ref = role.rolegroup_ref(&trino, "default"); - let merged_config = trino.merged_config(&role, &rolegroup_ref, &[]).unwrap(); - let coordinators = trino.role(&role).unwrap(); + // Merge + validate via the shared production path; the role + role-group + // `jvmArgumentOverrides` end up merged in `product_specific_common_config`. + let rg = crate::controller::validate::merged_role_group_config( + &trino, + &TrinoRole::Coordinator, + "default", + &[], + ); - let product_version = trino.spec.image.product_version(); + let product_version = + u16::from_str(trino.spec.image.product_version()).expect("trino version as u16"); jvm_config( - u16::from_str(product_version).expect("trino version as u16"), - &merged_config, - &coordinators, - "default", + product_version, + &rg.config, + &rg.product_specific_common_config.jvm_argument_overrides, ) .unwrap() } diff --git a/rust/operator-binary/src/config/s3.rs b/rust/operator-binary/src/config/s3.rs index a403ce836..4705bb448 100644 --- a/rust/operator-binary/src/config/s3.rs +++ b/rust/operator-binary/src/config/s3.rs @@ -3,12 +3,12 @@ use std::collections::BTreeMap; use snafu::{ResultExt, Snafu}; use stackable_operator::{ client::Client, - commons::tls_verification::{CaCert, TlsServerVerification, TlsVerification}, + commons::tls_verification::{CaCert, TlsClientDetails, TlsServerVerification, TlsVerification}, crd::s3, k8s_openapi::api::core::v1::{Volume, VolumeMount}, }; -use crate::{command, crd::STACKABLE_CLIENT_TLS_DIR}; +use crate::{controller::build::command, crd::STACKABLE_CLIENT_TLS_DIR}; #[derive(Snafu, Debug)] pub enum Error { @@ -19,12 +19,37 @@ pub enum Error { #[snafu(display("trino does not support disabling the TLS verification of S3 servers"))] S3TlsNoVerificationNotSupported, +} - #[snafu(display("failed to convert data size for [{field}] to bytes"))] - QuantityConversion { - source: stackable_operator::memory::Error, - field: &'static str, - }, +/// Returned by [`s3_tls_truststore_commands`] when an S3 connection disables TLS verification, +/// which Trino does not support. +#[derive(Debug)] +pub struct S3TlsVerificationDisabled; + +/// Build the init-container commands that add an S3 server's CA certificate to the client +/// truststore. +/// +/// Returns an empty list when no extra trust setup is needed (TLS disabled, or WebPKI +/// verification), and [`S3TlsVerificationDisabled`] when TLS verification is turned off (which +/// Trino rejects). Shared by the spooling/exchange S3 config and the S3 catalog connection. +pub fn s3_tls_truststore_commands( + tls: &TlsClientDetails, +) -> Result, S3TlsVerificationDisabled> { + let Some(tls_config) = tls.tls.as_ref() else { + return Ok(Vec::new()); + }; + match &tls_config.verification { + TlsVerification::None {} => Err(S3TlsVerificationDisabled), + TlsVerification::Server(TlsServerVerification { + ca_cert: CaCert::WebPki {}, + }) => Ok(Vec::new()), + TlsVerification::Server(TlsServerVerification { + ca_cert: CaCert::SecretClass(_), + }) => Ok(tls + .tls_ca_cert_mount_path() + .map(|ca_cert| command::add_cert_to_truststore(&ca_cert, STACKABLE_CLIENT_TLS_DIR)) + .unwrap_or_default()), + } } pub struct ResolvedS3Config { @@ -97,24 +122,55 @@ impl ResolvedS3Config { ]); } - if let Some(tls) = s3_connection.tls.tls.as_ref() { - match &tls.verification { - TlsVerification::None {} => return S3TlsNoVerificationNotSupportedSnafu.fail(), - TlsVerification::Server(TlsServerVerification { - ca_cert: CaCert::WebPki {}, - }) => {} - TlsVerification::Server(TlsServerVerification { - ca_cert: CaCert::SecretClass(_), - }) => { - if let Some(ca_cert) = s3_connection.tls.tls_ca_cert_mount_path() { - resolved_config.init_container_extra_start_commands.extend( - command::add_cert_to_truststore(&ca_cert, STACKABLE_CLIENT_TLS_DIR), - ); - } - } - } - } + resolved_config.init_container_extra_start_commands.extend( + s3_tls_truststore_commands(&s3_connection.tls) + .map_err(|_| Error::S3TlsNoVerificationNotSupported)?, + ); Ok(resolved_config) } } + +#[cfg(test)] +mod tests { + use stackable_operator::commons::tls_verification::Tls; + + use super::*; + + fn tls_details(verification: Option) -> TlsClientDetails { + TlsClientDetails { + tls: verification.map(|verification| Tls { verification }), + } + } + + #[test] + fn no_tls_yields_no_truststore_commands() { + assert!( + s3_tls_truststore_commands(&tls_details(None)) + .unwrap() + .is_empty() + ); + } + + #[test] + fn webpki_verification_yields_no_truststore_commands() { + let details = tls_details(Some(TlsVerification::Server(TlsServerVerification { + ca_cert: CaCert::WebPki {}, + }))); + assert!(s3_tls_truststore_commands(&details).unwrap().is_empty()); + } + + #[test] + fn secret_class_verification_yields_truststore_commands() { + let details = tls_details(Some(TlsVerification::Server(TlsServerVerification { + ca_cert: CaCert::SecretClass("tls".to_string()), + }))); + assert!(!s3_tls_truststore_commands(&details).unwrap().is_empty()); + } + + #[test] + fn disabled_verification_is_rejected() { + let details = tls_details(Some(TlsVerification::None {})); + assert!(s3_tls_truststore_commands(&details).is_err()); + } +} diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs deleted file mode 100644 index 7176a2652..000000000 --- a/rust/operator-binary/src/controller.rs +++ /dev/null @@ -1,2001 +0,0 @@ -//! Ensures that `Pod`s are configured and running for each [`v1alpha1::TrinoCluster`] -use std::{ - collections::{BTreeMap, HashMap}, - convert::Infallible, - num::ParseIntError, - str::FromStr, - sync::Arc, -}; - -use const_format::concatcp; -use product_config::{ - self, ProductConfigManager, - types::PropertyNameKind, - writer::{PropertiesWriterError, to_java_properties_string}, -}; -use snafu::{OptionExt, ResultExt, Snafu}; -use stackable_operator::{ - builder::{ - self, - configmap::ConfigMapBuilder, - meta::ObjectMetaBuilder, - pod::{ - PodBuilder, - container::ContainerBuilder, - resources::ResourceRequirementsBuilder, - security::PodSecurityContextBuilder, - volume::{SecretFormat, SecretOperatorVolumeSourceBuilder, VolumeBuilder}, - }, - }, - cli::OperatorEnvironmentOptions, - cluster_resources::{ClusterResourceApplyStrategy, ClusterResources}, - commons::{ - product_image_selection::ResolvedProductImage, random_secret_creation, - rbac::build_rbac_resources, secret_class::SecretClassVolumeProvisionParts, - }, - constants::RESTART_CONTROLLER_ENABLED_LABEL, - k8s_openapi::{ - DeepMerge, - api::{ - apps::v1::{StatefulSet, StatefulSetSpec}, - core::v1::{ - ConfigMap, ConfigMapVolumeSource, ContainerPort, EnvVar, EnvVarSource, ExecAction, - HTTPGetAction, Probe, SecretKeySelector, Volume, - }, - }, - apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString}, - }, - kube::{ - Resource, ResourceExt, - core::{DeserializeGuard, error_boundary}, - runtime::controller::Action, - }, - kvp::{Annotation, Annotations, Labels, ObjectLabels}, - logging::controller::ReconcilerError, - memory::{BinaryMultiple, MemoryQuantity}, - product_logging::{ - self, - framework::LoggingError, - spec::{ - ConfigMapLogConfig, ContainerLogConfig, ContainerLogConfigChoice, - CustomContainerLogConfig, - }, - }, - role_utils::{GenericRoleConfig, RoleGroupRef}, - shared::time::Duration, - status::condition::{ - compute_conditions, operations::ClusterOperationsConditionBuilder, - statefulset::StatefulSetConditionBuilder, - }, - utils::cluster_info::KubernetesClusterInfo, -}; -use strum::{EnumDiscriminants, IntoStaticStr}; - -mod dereference; -mod validate; - -use crate::{ - authentication::TrinoAuthenticationConfig, - authorization::opa::{OPA_TLS_VOLUME_NAME, TrinoOpaConfig}, - catalog::config::CatalogConfig, - command, - config::{self, client_protocol, fault_tolerant_execution}, - crd::{ - ACCESS_CONTROL_PROPERTIES, APP_NAME, CONFIG_DIR_NAME, CONFIG_PROPERTIES, Container, - DISCOVERY_URI, ENV_INTERNAL_SECRET, ENV_SPOOLING_SECRET, EXCHANGE_MANAGER_PROPERTIES, - HTTP_PORT, HTTP_PORT_NAME, HTTPS_PORT, HTTPS_PORT_NAME, JVM_CONFIG, - JVM_SECURITY_PROPERTIES, LOG_PROPERTIES, MAX_TRINO_LOG_FILES_SIZE, METRICS_PORT, - METRICS_PORT_NAME, NODE_PROPERTIES, RW_CONFIG_DIR_NAME, SPOOLING_MANAGER_PROPERTIES, - STACKABLE_CLIENT_TLS_DIR, STACKABLE_INTERNAL_TLS_DIR, STACKABLE_MOUNT_INTERNAL_TLS_DIR, - STACKABLE_MOUNT_SERVER_TLS_DIR, STACKABLE_SERVER_TLS_DIR, STACKABLE_TLS_STORE_PASSWORD, - TrinoRole, TrinoRoleType, - discovery::{TrinoDiscovery, TrinoDiscoveryProtocol, TrinoPodRef}, - v1alpha1, - }, - listener::{ - LISTENER_VOLUME_DIR, LISTENER_VOLUME_NAME, build_group_listener, build_group_listener_pvc, - group_listener_name, secret_volume_listener_scope, - }, - operations::{ - add_graceful_shutdown_config, graceful_shutdown_config_properties, pdb::add_pdbs, - }, - product_logging::{get_log_properties, get_vector_toml}, - service::{build_rolegroup_headless_service, build_rolegroup_metrics_service}, -}; - -pub struct Ctx { - pub client: stackable_operator::client::Client, - pub product_config: ProductConfigManager, - pub operator_environment: OperatorEnvironmentOptions, -} - -pub const OPERATOR_NAME: &str = "trino.stackable.tech"; -pub const CONTROLLER_NAME: &str = "trinocluster"; -pub const FULL_CONTROLLER_NAME: &str = concatcp!(CONTROLLER_NAME, '.', OPERATOR_NAME); - -pub const STACKABLE_LOG_DIR: &str = "/stackable/log"; -pub const STACKABLE_LOG_CONFIG_DIR: &str = "/stackable/log_config"; - -pub const MAX_PREPARE_LOG_FILE_SIZE: MemoryQuantity = MemoryQuantity { - value: 1.0, - unit: BinaryMultiple::Mebi, -}; - -pub(super) const CONTAINER_IMAGE_BASE_NAME: &str = "trino"; - -#[derive(Snafu, Debug, EnumDiscriminants)] -#[strum_discriminants(derive(IntoStaticStr))] -#[allow(clippy::enum_variant_names)] -pub enum Error { - #[snafu(display("missing secret lifetime"))] - MissingSecretLifetime, - - #[snafu(display("failed to create cluster resources"))] - CreateClusterResources { - source: stackable_operator::cluster_resources::Error, - }, - - #[snafu(display("failed to delete orphaned resources"))] - DeleteOrphanedResources { - source: stackable_operator::cluster_resources::Error, - }, - - #[snafu(display("failed to apply Service for {}", rolegroup))] - ApplyRoleGroupService { - source: stackable_operator::cluster_resources::Error, - rolegroup: RoleGroupRef, - }, - - #[snafu(display("failed to build ConfigMap for {}", rolegroup))] - BuildRoleGroupConfig { - source: stackable_operator::builder::configmap::Error, - rolegroup: RoleGroupRef, - }, - - #[snafu(display("failed to apply ConfigMap for {}", rolegroup))] - ApplyRoleGroupConfig { - source: stackable_operator::cluster_resources::Error, - rolegroup: RoleGroupRef, - }, - - #[snafu(display("failed to apply StatefulSet for {}", rolegroup))] - ApplyRoleGroupStatefulSet { - source: stackable_operator::cluster_resources::Error, - rolegroup: RoleGroupRef, - }, - - #[snafu(display("object is missing metadata to build owner reference"))] - ObjectMissingMetadataForOwnerRef { - source: stackable_operator::builder::meta::Error, - }, - - #[snafu(display("failed to format runtime properties"))] - FailedToWriteJavaProperties { source: PropertiesWriterError }, - - #[snafu(display("failed to parse role: {source}"))] - FailedToParseRole { source: strum::ParseError }, - - #[snafu(display("internal operator failure: {source}"))] - InternalOperatorFailure { source: crate::crd::Error }, - - #[snafu(display("no coordinator pods found for discovery"))] - MissingCoordinatorPods, - - #[snafu(display("illegal container name: [{container_name}]"))] - IllegalContainerName { - source: stackable_operator::builder::pod::container::Error, - container_name: String, - }, - - #[snafu(display("failed to resolve and merge config for role and role group"))] - FailedToResolveConfig { source: crate::crd::Error }, - - #[snafu(display("vector agent is enabled but vector aggregator ConfigMap is missing"))] - VectorAggregatorConfigMapMissing, - - #[snafu(display("failed to build vector container"))] - BuildVectorContainer { source: LoggingError }, - - #[snafu(display("failed to add the logging configuration to the ConfigMap [{cm_name}]"))] - InvalidLoggingConfig { - source: crate::product_logging::Error, - cm_name: String, - }, - - #[snafu(display("failed to patch service account"))] - ApplyServiceAccount { - source: stackable_operator::cluster_resources::Error, - }, - - #[snafu(display("failed to patch role binding"))] - ApplyRoleBinding { - source: stackable_operator::cluster_resources::Error, - }, - - #[snafu(display("failed to update status"))] - ApplyStatus { - source: stackable_operator::client::Error, - }, - - #[snafu(display("failed to build RBAC resources"))] - BuildRbacResources { - source: stackable_operator::commons::rbac::Error, - }, - - #[snafu(display("failed to serialize [{JVM_SECURITY_PROPERTIES}] for {}", rolegroup))] - JvmSecurityProperties { - source: PropertiesWriterError, - rolegroup: String, - }, - - #[snafu(display("failed to create PodDisruptionBudget"))] - FailedToCreatePdb { - source: crate::operations::pdb::Error, - }, - - #[snafu(display("failed to configure graceful shutdown"))] - GracefulShutdown { - source: crate::operations::graceful_shutdown::Error, - }, - - #[snafu(display("failed to get required Labels"))] - GetRequiredLabels { - source: - stackable_operator::kvp::KeyValuePairError, - }, - - #[snafu(display("failed to build Labels"))] - LabelBuild { - source: stackable_operator::kvp::LabelError, - }, - - #[snafu(display("failed to build Annotation"))] - AnnotationBuild { - source: stackable_operator::kvp::KeyValuePairError, - }, - - #[snafu(display("failed to build Metadata"))] - MetadataBuild { - source: stackable_operator::builder::meta::Error, - }, - - #[snafu(display("failed to build TLS certificate SecretClass Volume"))] - TlsCertSecretClassVolumeBuild { - source: stackable_operator::builder::pod::volume::SecretOperatorVolumeSourceBuilderError, - }, - - #[snafu(display("failed to build JVM config"))] - FailedToCreateJvmConfig { source: crate::config::jvm::Error }, - - #[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("invalid TrinoCluster object"))] - InvalidTrinoCluster { - source: error_boundary::InvalidObject, - }, - - #[snafu(display("failed to dereference resources"))] - Dereference { source: dereference::Error }, - - #[snafu(display("failed to validate cluster"))] - ValidateCluster { source: validate::Error }, - - #[snafu(display("failed to read role"))] - ReadRole { source: crate::crd::Error }, - - #[snafu(display("invalid Trino authentication"))] - InvalidAuthenticationConfig { - source: crate::authentication::Error, - }, - - #[snafu(display("unable to parse Trino version: {product_version:?}"))] - ParseTrinoVersion { - source: ParseIntError, - product_version: String, - }, - - #[snafu(display("failed to apply group listener"))] - ApplyGroupListener { - source: stackable_operator::cluster_resources::Error, - }, - - #[snafu(display("failed to configure listener"))] - ListenerConfiguration { source: crate::listener::Error }, - - #[snafu(display("failed to configure service"))] - ServiceConfiguration { source: crate::service::Error }, - - #[snafu(display("failed to create internal secret"))] - CreateInternalSecret { - source: random_secret_creation::Error, - }, -} - -type Result = std::result::Result; - -impl ReconcilerError for Error { - fn category(&self) -> &'static str { - ErrorDiscriminants::from(self).into() - } -} - -pub async fn reconcile_trino( - trino: Arc>, - ctx: Arc, -) -> Result { - tracing::info!("Starting reconcile"); - - let trino = trino - .0 - .as_ref() - .map_err(error_boundary::InvalidObject::clone) - .context(InvalidTrinoClusterSnafu)?; - let client = &ctx.client; - - // dereference (client required) - let dereferenced_objects = dereference::dereference(client, trino) - .await - .context(DereferenceSnafu)?; - - // validate (no client required) - let validated = validate::validate( - trino, - &dereferenced_objects, - &ctx.operator_environment, - &ctx.product_config, - ) - .context(ValidateClusterSnafu)?; - - let mut cluster_resources = ClusterResources::new( - APP_NAME, - OPERATOR_NAME, - CONTROLLER_NAME, - &trino.object_ref(&()), - ClusterResourceApplyStrategy::from(&trino.spec.cluster_operation), - &trino.spec.object_overrides, - ) - .context(CreateClusterResourcesSnafu)?; - - let (rbac_sa, rbac_rolebinding) = build_rbac_resources( - trino, - APP_NAME, - cluster_resources - .get_required_labels() - .context(GetRequiredLabelsSnafu)?, - ) - .context(BuildRbacResourcesSnafu)?; - - let rbac_sa = cluster_resources - .add(client, rbac_sa) - .await - .context(ApplyServiceAccountSnafu)?; - - cluster_resources - .add(client, rbac_rolebinding) - .await - .context(ApplyRoleBindingSnafu)?; - - random_secret_creation::create_random_secret_if_not_exists( - &shared_internal_secret_name(trino), - ENV_INTERNAL_SECRET, - 512, - trino, - client, - ) - .await - .context(CreateInternalSecretSnafu)?; - - // This secret is created even if spooling is not configured. - // Trino currently requires the secret to be exactly 256 bits long. - random_secret_creation::create_random_secret_if_not_exists( - &shared_spooling_secret_name(trino), - ENV_SPOOLING_SECRET, - 32, - trino, - client, - ) - .await - .context(CreateInternalSecretSnafu)?; - - let mut sts_cond_builder = StatefulSetConditionBuilder::default(); - - for (trino_role_str, role_config) in validated.validated_role_config { - let trino_role = TrinoRole::from_str(&trino_role_str).context(FailedToParseRoleSnafu)?; - let role = trino.role(&trino_role).context(ReadRoleSnafu)?; - for (role_group, config) in role_config { - let role_group_ref = trino_role.rolegroup_ref(trino, &role_group); - - let merged_config = trino - .merged_config( - &trino_role, - &role_group_ref, - &dereferenced_objects.catalog_definitions, - ) - .context(FailedToResolveConfigSnafu)?; - - let role_group_service_recommended_labels = build_recommended_labels( - trino, - &validated.image.app_version_label_value, - &role_group_ref.role, - &role_group_ref.role_group, - ); - - let role_group_service_selector = Labels::role_group_selector( - trino, - APP_NAME, - &role_group_ref.role, - &role_group_ref.role_group, - ) - .context(LabelBuildSnafu)?; - - let rg_headless_service = build_rolegroup_headless_service( - trino, - &role_group_ref, - role_group_service_recommended_labels.clone(), - role_group_service_selector.clone().into(), - ) - .context(ServiceConfigurationSnafu)?; - - let rg_metrics_service = build_rolegroup_metrics_service( - trino, - &role_group_ref, - role_group_service_recommended_labels, - role_group_service_selector.into(), - ) - .context(ServiceConfigurationSnafu)?; - - let rg_configmap = build_rolegroup_config_map( - trino, - &validated.image, - &role, - &trino_role, - &role_group_ref, - &config, - &merged_config, - &validated.trino_authentication_config, - &dereferenced_objects.trino_opa_config, - &client.kubernetes_cluster_info, - &dereferenced_objects.resolved_fte_config, - &dereferenced_objects.resolved_client_protocol_config, - )?; - let rg_catalog_configmap = build_rolegroup_catalog_config_map( - trino, - &validated.image, - &role_group_ref, - &dereferenced_objects.catalogs, - )?; - let rg_stateful_set = build_rolegroup_statefulset( - trino, - &trino_role, - &validated.image, - &role_group_ref, - &config, - &merged_config, - &validated.trino_authentication_config, - &dereferenced_objects.catalogs, - &rbac_sa.name_any(), - &dereferenced_objects.resolved_fte_config, - &dereferenced_objects.resolved_client_protocol_config, - &dereferenced_objects.trino_opa_config, - )?; - - cluster_resources - .add(client, rg_headless_service) - .await - .with_context(|_| ApplyRoleGroupServiceSnafu { - rolegroup: role_group_ref.clone(), - })?; - - cluster_resources - .add(client, rg_metrics_service) - .await - .with_context(|_| ApplyRoleGroupServiceSnafu { - rolegroup: role_group_ref.clone(), - })?; - - cluster_resources - .add(client, rg_configmap) - .await - .with_context(|_| ApplyRoleGroupConfigSnafu { - rolegroup: role_group_ref.clone(), - })?; - - cluster_resources - .add(client, rg_catalog_configmap) - .await - .with_context(|_| ApplyRoleGroupConfigSnafu { - rolegroup: role_group_ref.clone(), - })?; - - // Note: The StatefulSet needs to be applied after all ConfigMaps and Secrets it mounts - // to prevent unnecessary Pod restarts. - // See https://github.com/stackabletech/commons-operator/issues/111 for details. - sts_cond_builder.add( - cluster_resources - .add(client, rg_stateful_set) - .await - .with_context(|_| ApplyRoleGroupStatefulSetSnafu { - rolegroup: role_group_ref.clone(), - })?, - ); - } - - if let Some(listener_class) = trino_role.listener_class_name(trino) - && let Some(listener_group_name) = group_listener_name(trino, &trino_role) - { - let role_group_listener = build_group_listener( - trino, - build_recommended_labels( - trino, - &validated.image.app_version_label_value, - &trino_role_str, - "none", - ), - listener_class.to_string(), - listener_group_name, - ) - .context(ListenerConfigurationSnafu)?; - - cluster_resources - .add(client, role_group_listener) - .await - .context(ApplyGroupListenerSnafu)?; - } - - let role_config = trino.generic_role_config(&trino_role); - if let Some(GenericRoleConfig { - pod_disruption_budget: pdb, - }) = role_config - { - add_pdbs(pdb, trino, &trino_role, client, &mut cluster_resources) - .await - .context(FailedToCreatePdbSnafu)?; - } - } - - let cluster_operation_cond_builder = - ClusterOperationsConditionBuilder::new(&trino.spec.cluster_operation); - - let status = v1alpha1::TrinoClusterStatus { - conditions: compute_conditions( - trino, - &[&sts_cond_builder, &cluster_operation_cond_builder], - ), - }; - - cluster_resources - .delete_orphaned_resources(client) - .await - .context(DeleteOrphanedResourcesSnafu)?; - client - .apply_patch_status(OPERATOR_NAME, trino, &status) - .await - .context(ApplyStatusSnafu)?; - - Ok(Action::await_change()) -} - -/// The rolegroup [`ConfigMap`] configures the rolegroup based on the configuration given by the administrator -#[allow(clippy::too_many_arguments)] -fn build_rolegroup_config_map( - trino: &v1alpha1::TrinoCluster, - resolved_product_image: &ResolvedProductImage, - role: &TrinoRoleType, - trino_role: &TrinoRole, - rolegroup_ref: &RoleGroupRef, - config: &HashMap>, - merged_config: &v1alpha1::TrinoConfig, - trino_authentication_config: &TrinoAuthenticationConfig, - trino_opa_config: &Option, - cluster_info: &KubernetesClusterInfo, - resolved_fte_config: &Option, - resolved_spooling_config: &Option, -) -> Result { - let mut cm_conf_data = BTreeMap::new(); - - let product_version = &resolved_product_image.product_version; - let product_version = - u16::from_str(product_version).context(ParseTrinoVersionSnafu { product_version })?; - let jvm_config = config::jvm::jvm_config( - product_version, - merged_config, - role, - &rolegroup_ref.role_group, - ) - .context(FailedToCreateJvmConfigSnafu)?; - - // TODO: we support only one coordinator for now - let coordinator_ref: TrinoPodRef = trino - .coordinator_pods() - .context(InternalOperatorFailureSnafu)? - .next() - .context(MissingCoordinatorPodsSnafu)?; - - // Add additional config files for authentication - cm_conf_data.extend(trino_authentication_config.config_files(trino_role)); - - for (property_name_kind, config) in config { - // We used this temporary map to add all dynamically resolved (e.g. discovery config maps) - // properties. This will be extended with the merged role group properties (transformed_config) - // to respect all possible override settings. - let mut dynamic_resolved_config = BTreeMap::>::new(); - - let transformed_config: BTreeMap> = config - .iter() - .map(|(k, v)| (k.clone(), Some(v.clone()))) - .collect(); - - match property_name_kind { - PropertyNameKind::File(file_name) if file_name == CONFIG_PROPERTIES => { - // Add authentication properties (only required for the Coordinator) - dynamic_resolved_config.extend( - trino_authentication_config - .config_properties(trino_role) - .into_iter() - .map(|(k, v)| (k, Some(v))) - .collect::>>(), - ); - - let protocol = if trino.get_internal_tls().is_some() { - TrinoDiscoveryProtocol::Https - } else { - TrinoDiscoveryProtocol::Http - }; - - let discovery = TrinoDiscovery::new(&coordinator_ref, protocol); - dynamic_resolved_config.insert( - DISCOVERY_URI.to_string(), - Some(discovery.discovery_uri(cluster_info)), - ); - - dynamic_resolved_config - .extend(graceful_shutdown_config_properties(trino, trino_role)); - - // Add fault tolerant execution properties from resolved configuration - if let Some(resolved_fte) = resolved_fte_config { - dynamic_resolved_config.extend( - resolved_fte - .config_properties - .iter() - .map(|(k, v)| (k.clone(), Some(v.clone()))), - ); - } - - // Add spooling properties from resolved configuration - if let Some(resolved_spooling) = resolved_spooling_config { - dynamic_resolved_config.extend( - resolved_spooling - .config_properties - .iter() - .map(|(k, v)| (k.clone(), Some(v.clone()))), - ); - } - - // Add static properties and overrides - dynamic_resolved_config.extend(transformed_config); - - let config_properties = product_config::writer::to_java_properties_string( - dynamic_resolved_config.iter(), - ) - .context(FailedToWriteJavaPropertiesSnafu)?; - - cm_conf_data.insert(file_name.to_string(), config_properties); - } - - PropertyNameKind::File(file_name) if file_name == NODE_PROPERTIES => { - // Add static properties and overrides - dynamic_resolved_config.extend(transformed_config); - - let node_properties = product_config::writer::to_java_properties_string( - dynamic_resolved_config.iter(), - ) - .context(FailedToWriteJavaPropertiesSnafu)?; - - cm_conf_data.insert(file_name.to_string(), node_properties); - } - PropertyNameKind::File(file_name) if file_name == LOG_PROPERTIES => { - // No overrides required here, all settings can be set via logging options - if let Some(log_properties) = get_log_properties(&merged_config.logging) { - cm_conf_data.insert(file_name.to_string(), log_properties); - } - - if let Some(vector_toml) = get_vector_toml(rolegroup_ref, &merged_config.logging) - .context(InvalidLoggingConfigSnafu { - cm_name: rolegroup_ref.object_name(), - })? - { - cm_conf_data.insert( - product_logging::framework::VECTOR_CONFIG_FILE.to_string(), - vector_toml, - ); - } - } - PropertyNameKind::File(file_name) if file_name == ACCESS_CONTROL_PROPERTIES => { - if let Some(trino_opa_config) = trino_opa_config { - dynamic_resolved_config.extend(trino_opa_config.as_config()); - } - - // Add static properties and overrides - dynamic_resolved_config.extend(transformed_config); - - if !dynamic_resolved_config.is_empty() { - let access_control_properties = - product_config::writer::to_java_properties_string( - dynamic_resolved_config.iter(), - ) - .context(FailedToWriteJavaPropertiesSnafu)?; - - cm_conf_data.insert(file_name.to_string(), access_control_properties); - } - } - PropertyNameKind::File(file_name) if file_name == JVM_CONFIG => {} - PropertyNameKind::File(file_name) if file_name == SPOOLING_MANAGER_PROPERTIES => { - // Add automatic properties for the spooling protocol - if let Some(spooling_config) = resolved_spooling_config { - dynamic_resolved_config = spooling_config - .spooling_manager_properties - .iter() - .map(|(k, v)| (k.clone(), Some(v.clone()))) - .collect(); - } - - // Override automatic properties with user provided configuration for the spooling protocol - dynamic_resolved_config.extend(transformed_config); - - if !dynamic_resolved_config.is_empty() { - cm_conf_data.insert( - file_name.to_string(), - to_java_properties_string(dynamic_resolved_config.iter()) - .with_context(|_| FailedToWriteJavaPropertiesSnafu)?, - ); - } - } - PropertyNameKind::File(file_name) if file_name == EXCHANGE_MANAGER_PROPERTIES => { - // Add exchange manager properties from resolved fault tolerant execution configuration - if let Some(resolved_fte) = resolved_fte_config { - dynamic_resolved_config = resolved_fte - .exchange_manager_properties - .iter() - .map(|(k, v)| (k.clone(), Some(v.clone()))) - .collect(); - } - - // Override automatic properties with user provided configuration for the spooling protocol - dynamic_resolved_config.extend(transformed_config); - - if !dynamic_resolved_config.is_empty() { - cm_conf_data.insert( - file_name.to_string(), - to_java_properties_string(dynamic_resolved_config.iter()) - .with_context(|_| FailedToWriteJavaPropertiesSnafu)?, - ); - } - } - _ => {} - } - } - - cm_conf_data.insert(JVM_CONFIG.to_string(), jvm_config.to_string()); - - let jvm_sec_props: BTreeMap> = config - .get(&PropertyNameKind::File(JVM_SECURITY_PROPERTIES.to_string())) - .cloned() - .unwrap_or_default() - .into_iter() - .map(|(k, v)| (k, Some(v))) - .collect(); - - cm_conf_data.insert( - JVM_SECURITY_PROPERTIES.to_string(), - to_java_properties_string(jvm_sec_props.iter()).with_context(|_| { - JvmSecurityPropertiesSnafu { - rolegroup: rolegroup_ref.role_group.clone(), - } - })?, - ); - - ConfigMapBuilder::new() - .metadata( - ObjectMetaBuilder::new() - .name_and_namespace(trino) - .name(rolegroup_ref.object_name()) - .ownerreference_from_resource(trino, None, Some(true)) - .context(ObjectMissingMetadataForOwnerRefSnafu)? - .with_recommended_labels(&build_recommended_labels( - trino, - &resolved_product_image.app_version_label_value, - &rolegroup_ref.role, - &rolegroup_ref.role_group, - )) - .context(MetadataBuildSnafu)? - .build(), - ) - .data(cm_conf_data) - .build() - .with_context(|_| BuildRoleGroupConfigSnafu { - rolegroup: rolegroup_ref.clone(), - }) -} - -/// The rolegroup catalog [`ConfigMap`] configures the rolegroup catalog based on the configuration -/// given by the administrator -fn build_rolegroup_catalog_config_map( - trino: &v1alpha1::TrinoCluster, - resolved_product_image: &ResolvedProductImage, - rolegroup_ref: &RoleGroupRef, - catalogs: &[CatalogConfig], -) -> Result { - ConfigMapBuilder::new() - .metadata( - ObjectMetaBuilder::new() - .name_and_namespace(trino) - .name(format!("{}-catalog", rolegroup_ref.object_name())) - .ownerreference_from_resource(trino, None, Some(true)) - .context(ObjectMissingMetadataForOwnerRefSnafu)? - .with_recommended_labels(&build_recommended_labels( - trino, - &resolved_product_image.app_version_label_value, - &rolegroup_ref.role, - &rolegroup_ref.role_group, - )) - .context(MetadataBuildSnafu)? - .build(), - ) - .data( - catalogs - .iter() - .map(|catalog| { - let catalog_props = catalog - .properties - .iter() - .map(|(k, v)| (k.to_string(), Some(v.to_string()))) - .collect::>(); - Ok(( - format!("{}.properties", catalog.name), - // false positive https://github.com/rust-lang/rust-clippy/issues/9280 - // we need the tuple (&String, &Option) which the extra map is doing. - // Removing the map changes the type to &(String, Option) - #[allow(clippy::map_identity)] - product_config::writer::to_java_properties_string( - catalog_props.iter().map(|(k, v)| (k, v)), - ) - .context(FailedToWriteJavaPropertiesSnafu)?, - )) - }) - .collect::>()?, - ) - .build() - .with_context(|_| BuildRoleGroupConfigSnafu { - rolegroup: rolegroup_ref.clone(), - }) -} - -/// The 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 [`stackable_operator::k8s_openapi::api::core::v1::Service`] (from [`build_rolegroup_headless_service`]). -#[allow(clippy::too_many_arguments)] -fn build_rolegroup_statefulset( - trino: &v1alpha1::TrinoCluster, - trino_role: &TrinoRole, - resolved_product_image: &ResolvedProductImage, - role_group_ref: &RoleGroupRef, - config: &HashMap>, - merged_config: &v1alpha1::TrinoConfig, - trino_authentication_config: &TrinoAuthenticationConfig, - catalogs: &[CatalogConfig], - sa_name: &str, - resolved_fte_config: &Option, - resolved_spooling_config: &Option, - trino_opa_config: &Option, -) -> Result { - let role = trino - .role(trino_role) - .context(InternalOperatorFailureSnafu)?; - let rolegroup = trino - .rolegroup(role_group_ref) - .context(InternalOperatorFailureSnafu)?; - - let mut pod_builder = PodBuilder::new(); - - let prepare_container_name = Container::Prepare.to_string(); - let mut cb_prepare = ContainerBuilder::new(&prepare_container_name).with_context(|_| { - IllegalContainerNameSnafu { - container_name: prepare_container_name.clone(), - } - })?; - - let trino_container_name = Container::Trino.to_string(); - let mut cb_trino = ContainerBuilder::new(&trino_container_name).with_context(|_| { - IllegalContainerNameSnafu { - container_name: trino_container_name.clone(), - } - })?; - - // additional authentication env vars - let mut env = trino_authentication_config.env_vars(trino_role, &Container::Trino); - - let internal_secret_name = shared_internal_secret_name(trino); - env.push(env_var_from_secret( - &internal_secret_name, - None, - ENV_INTERNAL_SECRET, - )); - - let spooling_secret_name = shared_spooling_secret_name(trino); - env.push(env_var_from_secret( - &spooling_secret_name, - None, - ENV_SPOOLING_SECRET, - )); - - trino_authentication_config - .add_authentication_pod_and_volume_config( - trino_role, - &mut pod_builder, - &mut cb_prepare, - &mut cb_trino, - ) - .context(InvalidAuthenticationConfigSnafu)?; - add_graceful_shutdown_config( - trino, - trino_role, - merged_config, - &mut pod_builder, - &mut cb_trino, - ) - .context(GracefulShutdownSnafu)?; - - // Add the needed stuff for catalogs - env.extend( - catalogs - .iter() - .flat_map(|catalog| &catalog.env_bindings) - .cloned(), - ); - - // Needed by the `containerdebug` process to log it's tracing information to. - // This process runs in the background of the `trino` container. - // See command::container_trino_args() for how it's called. - env.push(EnvVar { - name: "CONTAINERDEBUG_LOG_DIRECTORY".into(), - value: Some(format!("{STACKABLE_LOG_DIR}/containerdebug")), - ..EnvVar::default() - }); - - // Finally add the user defined envOverrides properties. - env.extend( - config - .get(&PropertyNameKind::Env) - .into_iter() - .flatten() - .map(|(k, v)| EnvVar { - name: k.clone(), - value: Some(v.clone()), - ..EnvVar::default() - }), - ); - - let requested_secret_lifetime = merged_config - .requested_secret_lifetime - .context(MissingSecretLifetimeSnafu)?; - - // add volume mounts depending on the client tls, internal tls, catalogs and authentication - tls_volume_mounts( - trino, - trino_role, - &mut pod_builder, - &mut cb_prepare, - &mut cb_trino, - catalogs, - &requested_secret_lifetime, - resolved_fte_config, - resolved_spooling_config, - trino_opa_config, - )?; - - let mut prepare_args = vec![]; - if let Some(ContainerLogConfig { - choice: Some(ContainerLogConfigChoice::Automatic(log_config)), - }) = merged_config.logging.containers.get(&Container::Prepare) - { - prepare_args.push(product_logging::framework::capture_shell_output( - STACKABLE_LOG_DIR, - &prepare_container_name, - log_config, - )); - } - - prepare_args.extend(command::container_prepare_args( - trino, - catalogs, - merged_config, - resolved_fte_config, - resolved_spooling_config, - )); - - prepare_args - .extend(trino_authentication_config.commands(&TrinoRole::Coordinator, &Container::Prepare)); - - // Add OPA TLS certificate to truststore if configured - if let Some(tls_mount_path) = trino_opa_config - .as_ref() - .and_then(|opa_config| opa_config.tls_mount_path()) - { - prepare_args.extend(command::add_cert_to_truststore( - format!("{}/ca.crt", tls_mount_path).as_str(), - STACKABLE_CLIENT_TLS_DIR, - )); - } - - let container_prepare = cb_prepare - .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![prepare_args.join("\n")]) - .add_volume_mount("rwconfig", RW_CONFIG_DIR_NAME) - .context(AddVolumeMountSnafu)? - .add_volume_mount("log-config", STACKABLE_LOG_CONFIG_DIR) - .context(AddVolumeMountSnafu)? - .add_volume_mount("log", STACKABLE_LOG_DIR) - .context(AddVolumeMountSnafu)? - .resources( - ResourceRequirementsBuilder::new() - .with_cpu_request("500m") - .with_cpu_limit("2000m") - .with_memory_request("4Gi") - .with_memory_limit("4Gi") - .build(), - ) - .build(); - - let mut persistent_volume_claims = vec![]; - // Add listener - if let Some(group_listener_name) = group_listener_name(trino, trino_role) { - cb_trino - .add_volume_mount(LISTENER_VOLUME_NAME, LISTENER_VOLUME_DIR) - .context(AddVolumeMountSnafu)?; - - // Used for PVC templates that cannot be modified once they are deployed - let unversioned_recommended_labels = Labels::recommended(&build_recommended_labels( - trino, - // A version value is required, and we do want to use the "recommended" format for the other desired labels - "none", - &role_group_ref.role, - &role_group_ref.role_group, - )) - .context(LabelBuildSnafu)?; - - persistent_volume_claims.push( - build_group_listener_pvc(&group_listener_name, &unversioned_recommended_labels) - .context(ListenerConfigurationSnafu)?, - ); - } - - let container_trino = cb_trino - .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![ - command::container_trino_args(trino_authentication_config, catalogs).join("\n"), - ]) - .add_env_vars(env) - .add_volume_mount("config", CONFIG_DIR_NAME) - .context(AddVolumeMountSnafu)? - .add_volume_mount("rwconfig", RW_CONFIG_DIR_NAME) - .context(AddVolumeMountSnafu)? - .add_volume_mount("catalog", format!("{}/catalog", CONFIG_DIR_NAME)) - .context(AddVolumeMountSnafu)? - .add_volume_mount("log", STACKABLE_LOG_DIR) - .context(AddVolumeMountSnafu)? - .add_container_ports(container_ports(trino)) - .resources(merged_config.resources.clone().into()) - // The probes are set on coordinators and workers - .startup_probe(startup_probe(trino)) - .readiness_probe(readiness_probe(trino)) - .liveness_probe(liveness_probe(trino)) - .build(); - - // add trino container first to better default into that container (e.g. instead of vector) - pod_builder.add_container(container_trino); - - // add password-update container if required - trino_authentication_config.add_authentication_containers(trino_role, &mut pod_builder); - - if let Some(ContainerLogConfig { - choice: - Some(ContainerLogConfigChoice::Custom(CustomContainerLogConfig { - custom: ConfigMapLogConfig { config_map }, - })), - }) = merged_config.logging.containers.get(&Container::Trino) - { - pod_builder - .add_volume(Volume { - name: "log-config".to_string(), - config_map: Some(ConfigMapVolumeSource { - name: config_map.into(), - ..ConfigMapVolumeSource::default() - }), - ..Volume::default() - }) - .context(AddVolumeSnafu)?; - } else { - pod_builder - .add_volume(Volume { - name: "log-config".to_string(), - config_map: Some(ConfigMapVolumeSource { - name: role_group_ref.object_name(), - ..ConfigMapVolumeSource::default() - }), - ..Volume::default() - }) - .context(AddVolumeSnafu)?; - } - - if merged_config.logging.enable_vector_agent { - match &trino.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", - merged_config.logging.containers.get(&Container::Vector), - 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(BuildVectorContainerSnafu)?, - ); - } - None => { - VectorAggregatorConfigMapMissingSnafu.fail()?; - } - } - } - - let metadata = ObjectMetaBuilder::new() - .with_recommended_labels(&build_recommended_labels( - trino, - &resolved_product_image.app_version_label_value, - &role_group_ref.role, - &role_group_ref.role_group, - )) - .context(MetadataBuildSnafu)? - .with_annotation( - // This is actually used by some kuttl tests (as they don't specify the container explicitly) - Annotation::try_from(("kubectl.kubernetes.io/default-container", "trino")) - .context(AnnotationBuildSnafu)?, - ) - .build(); - - pod_builder - .metadata(metadata) - .image_pull_secrets_from_product_image(resolved_product_image) - .affinity(&merged_config.affinity) - .add_init_container(container_prepare) - .add_volume(Volume { - name: "config".to_string(), - config_map: Some(ConfigMapVolumeSource { - name: role_group_ref.object_name(), - ..ConfigMapVolumeSource::default() - }), - ..Volume::default() - }) - .context(AddVolumeSnafu)? - .add_empty_dir_volume("rwconfig", None) - .context(AddVolumeSnafu)? - .add_volume(Volume { - name: "catalog".to_string(), - config_map: Some(ConfigMapVolumeSource { - name: format!("{}-catalog", role_group_ref.object_name()), - ..ConfigMapVolumeSource::default() - }), - ..Volume::default() - }) - .context(AddVolumeSnafu)? - .add_empty_dir_volume( - "log", - Some(product_logging::framework::calculate_log_volume_size_limit( - &[MAX_TRINO_LOG_FILES_SIZE, MAX_PREPARE_LOG_FILE_SIZE], - )), - ) - .context(AddVolumeSnafu)? - .service_account_name(sa_name) - .security_context(PodSecurityContextBuilder::new().fs_group(1000).build()); - - let mut pod_template = pod_builder.build_template(); - pod_template.merge_from(role.config.pod_overrides.clone()); - pod_template.merge_from(rolegroup.config.pod_overrides.clone()); - - let ignore_secret_annotations = trino_authentication_config - .hot_reloaded_secrets() - .iter() - .enumerate() - .map(|(i, secret_name)| { - ( - format!("restarter.stackable.tech/ignore-secret.{i}"), - secret_name, - ) - }) - .collect::>(); - - let annotations = - Annotations::try_from(ignore_secret_annotations).context(AnnotationBuildSnafu)?; - - Ok(StatefulSet { - metadata: ObjectMetaBuilder::new() - .name_and_namespace(trino) - .name(role_group_ref.object_name()) - .ownerreference_from_resource(trino, None, Some(true)) - .context(ObjectMissingMetadataForOwnerRefSnafu)? - .with_recommended_labels(&build_recommended_labels( - trino, - &resolved_product_image.app_version_label_value, - &role_group_ref.role, - &role_group_ref.role_group, - )) - .context(MetadataBuildSnafu)? - .with_label(RESTART_CONTROLLER_ENABLED_LABEL.to_owned()) - .with_annotations(annotations) - .build(), - spec: Some(StatefulSetSpec { - pod_management_policy: Some("Parallel".to_string()), - replicas: rolegroup.replicas.map(i32::from), - selector: LabelSelector { - match_labels: Some( - Labels::role_group_selector( - trino, - APP_NAME, - &role_group_ref.role, - &role_group_ref.role_group, - ) - .context(LabelBuildSnafu)? - .into(), - ), - ..LabelSelector::default() - }, - service_name: Some(role_group_ref.rolegroup_headless_service_name()), - template: pod_template, - volume_claim_templates: Some(persistent_volume_claims), - ..StatefulSetSpec::default() - }), - status: None, - }) -} - -pub fn error_policy( - _obj: Arc>, - error: &Error, - _ctx: Arc, -) -> Action { - match error { - Error::InvalidTrinoCluster { .. } => Action::await_change(), - _ => Action::requeue(*Duration::from_secs(5)), - } -} - -/// Give a secret name and an optional key in the secret to use. -/// The value from the key will be set into the given env var name. -/// If not secret key is given, the env var name will be used as the secret key. -fn env_var_from_secret(secret_name: &str, secret_key: Option<&str>, env_var: &str) -> EnvVar { - EnvVar { - name: env_var.to_string(), - value_from: Some(EnvVarSource { - secret_key_ref: Some(SecretKeySelector { - optional: Some(false), - name: secret_name.to_string(), - key: secret_key.unwrap_or(env_var).to_string(), - }), - ..EnvVarSource::default() - }), - ..EnvVar::default() - } -} - -fn build_recommended_labels<'a>( - owner: &'a v1alpha1::TrinoCluster, - app_version: &'a str, - role: &'a str, - role_group: &'a str, -) -> ObjectLabels<'a, v1alpha1::TrinoCluster> { - ObjectLabels { - owner, - app_name: APP_NAME, - app_version, - operator_name: OPERATOR_NAME, - controller_name: CONTROLLER_NAME, - role, - role_group, - } -} - -fn shared_internal_secret_name(trino: &v1alpha1::TrinoCluster) -> String { - format!("{}-internal-secret", trino.name_any()) -} - -fn shared_spooling_secret_name(trino: &v1alpha1::TrinoCluster) -> String { - format!("{}-spooling-secret", trino.name_any()) -} - -fn container_ports(trino: &v1alpha1::TrinoCluster) -> 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() - }]; - - if trino.expose_http_port() { - ports.push(ContainerPort { - name: Some(HTTP_PORT_NAME.to_string()), - container_port: HTTP_PORT.into(), - protocol: Some("TCP".to_string()), - ..ContainerPort::default() - }) - } - - if trino.expose_https_port() { - ports.push(ContainerPort { - name: Some(HTTPS_PORT_NAME.to_string()), - container_port: HTTPS_PORT.into(), - protocol: Some("TCP".to_string()), - ..ContainerPort::default() - }); - } - - ports -} - -fn startup_probe(trino: &v1alpha1::TrinoCluster) -> Probe { - Probe { - exec: Some(finished_starting_probe(trino)), - period_seconds: Some(5), - // Give the coordinator or worker 10 minutes to start up - failure_threshold: Some(120), - timeout_seconds: Some(3), - ..Default::default() - } -} - -fn readiness_probe(trino: &v1alpha1::TrinoCluster) -> Probe { - Probe { - http_get: Some(http_get_probe(trino)), - period_seconds: Some(5), - failure_threshold: Some(1), - timeout_seconds: Some(3), - ..Probe::default() - } -} - -fn liveness_probe(trino: &v1alpha1::TrinoCluster) -> Probe { - Probe { - http_get: Some(http_get_probe(trino)), - period_seconds: Some(5), - // Coordinators are currently not highly available, so you always have a singe instance. - // Restarting it causes all queries to fail, so let's not restart it directly after the first - // probe failure, but wait for 3 failures - // NOTE: This also applies to workers - failure_threshold: Some(3), - timeout_seconds: Some(3), - ..Probe::default() - } -} - -/// Check that `/v1/info` returns `200`. -/// -/// This is the same probe as the [upstream helm-chart](https://github.com/trinodb/charts/blob/7cd0a7bff6c52e0ee6ca6d5394cd72c150ad4379/charts/trino/templates/deployment-coordinator.yaml#L214) -/// is using. -fn http_get_probe(trino: &v1alpha1::TrinoCluster) -> HTTPGetAction { - let (schema, port_name) = if trino.expose_https_port() { - ("HTTPS", HTTPS_PORT_NAME) - } else { - ("HTTP", HTTP_PORT_NAME) - }; - - HTTPGetAction { - port: IntOrString::String(port_name.to_string()), - scheme: Some(schema.to_string()), - path: Some("/v1/info".to_string()), - ..Default::default() - } -} - -/// Wait until `/v1/info` returns `"starting":false`. -/// -/// This probe works on coordinators and workers. -fn finished_starting_probe(trino: &v1alpha1::TrinoCluster) -> ExecAction { - let port = trino.exposed_port(); - let schema = if trino.expose_https_port() { - "https" - } else { - "http" - }; - - ExecAction { - command: Some(vec![ - "/bin/bash".to_string(), - "-x".to_string(), - "-euo".to_string(), - "pipefail".to_string(), - "-c".to_string(), - format!( - "curl --fail --insecure {schema}://127.0.0.1:{port}/v1/info | grep --silent '\\\"starting\\\":false'" - ), - ]), - } -} - -fn create_tls_volume( - volume_name: &str, - tls_secret_class: &str, - requested_secret_lifetime: &Duration, - listener_scope: Option, -) -> Result { - let mut secret_volume_source_builder = SecretOperatorVolumeSourceBuilder::new( - tls_secret_class, - SecretClassVolumeProvisionParts::PublicPrivate, - ); - - secret_volume_source_builder - .with_pod_scope() - .with_format(SecretFormat::TlsPkcs12) - .with_tls_pkcs12_password(STACKABLE_TLS_STORE_PASSWORD) - .with_auto_tls_cert_lifetime(*requested_secret_lifetime); - - if let Some(listener_scope) = &listener_scope { - secret_volume_source_builder.with_listener_volume_scope(listener_scope); - } - - Ok(VolumeBuilder::new(volume_name) - .ephemeral( - secret_volume_source_builder - .build() - .context(TlsCertSecretClassVolumeBuildSnafu)?, - ) - .build()) -} - -#[allow(clippy::too_many_arguments)] -fn tls_volume_mounts( - trino: &v1alpha1::TrinoCluster, - trino_role: &TrinoRole, - pod_builder: &mut PodBuilder, - cb_prepare: &mut ContainerBuilder, - cb_trino: &mut ContainerBuilder, - catalogs: &[CatalogConfig], - requested_secret_lifetime: &Duration, - resolved_fte_config: &Option, - resolved_spooling_config: &Option, - trino_opa_config: &Option, -) -> Result<()> { - if let Some(server_tls) = trino.get_server_tls() { - cb_prepare - .add_volume_mount("server-tls-mount", STACKABLE_MOUNT_SERVER_TLS_DIR) - .context(AddVolumeMountSnafu)?; - cb_trino - .add_volume_mount("server-tls-mount", STACKABLE_MOUNT_SERVER_TLS_DIR) - .context(AddVolumeMountSnafu)?; - pod_builder - .add_volume(create_tls_volume( - "server-tls-mount", - server_tls, - requested_secret_lifetime, - // add listener - secret_volume_listener_scope(trino_role), - )?) - .context(AddVolumeSnafu)?; - } - - cb_prepare - .add_volume_mount("server-tls", STACKABLE_SERVER_TLS_DIR) - .context(AddVolumeMountSnafu)?; - cb_trino - .add_volume_mount("server-tls", STACKABLE_SERVER_TLS_DIR) - .context(AddVolumeMountSnafu)?; - pod_builder - .add_empty_dir_volume("server-tls", None) - .context(AddVolumeSnafu)?; - - cb_prepare - .add_volume_mount("client-tls", STACKABLE_CLIENT_TLS_DIR) - .context(AddVolumeMountSnafu)?; - cb_trino - .add_volume_mount("client-tls", STACKABLE_CLIENT_TLS_DIR) - .context(AddVolumeMountSnafu)?; - pod_builder - .add_empty_dir_volume("client-tls", None) - .context(AddVolumeSnafu)?; - - if let Some(internal_tls) = trino.get_internal_tls() { - cb_prepare - .add_volume_mount("internal-tls-mount", STACKABLE_MOUNT_INTERNAL_TLS_DIR) - .context(AddVolumeMountSnafu)?; - cb_trino - .add_volume_mount("internal-tls-mount", STACKABLE_MOUNT_INTERNAL_TLS_DIR) - .context(AddVolumeMountSnafu)?; - pod_builder - .add_volume(create_tls_volume( - "internal-tls-mount", - internal_tls, - requested_secret_lifetime, - None, - )?) - .context(AddVolumeSnafu)?; - - cb_prepare - .add_volume_mount("internal-tls", STACKABLE_INTERNAL_TLS_DIR) - .context(AddVolumeMountSnafu)?; - cb_trino - .add_volume_mount("internal-tls", STACKABLE_INTERNAL_TLS_DIR) - .context(AddVolumeMountSnafu)?; - pod_builder - .add_empty_dir_volume("internal-tls", None) - .context(AddVolumeSnafu)?; - } - - // catalogs - for catalog in catalogs { - cb_prepare - .add_volume_mounts(catalog.volume_mounts.clone()) - .context(AddVolumeMountSnafu)?; - cb_trino - .add_volume_mounts(catalog.volume_mounts.clone()) - .context(AddVolumeMountSnafu)?; - pod_builder - .add_volumes(catalog.volumes.clone()) - .context(AddVolumeSnafu)?; - } - - // Add OPA TLS certs if configured - if let Some((tls_secret_class, tls_mount_path)) = - trino_opa_config.as_ref().and_then(|opa_config| { - opa_config - .tls_secret_class - .as_ref() - .zip(opa_config.tls_mount_path()) - }) - { - cb_prepare - .add_volume_mount(OPA_TLS_VOLUME_NAME, &tls_mount_path) - .context(AddVolumeMountSnafu)?; - - let opa_tls_volume = VolumeBuilder::new(OPA_TLS_VOLUME_NAME) - .ephemeral( - SecretOperatorVolumeSourceBuilder::new( - tls_secret_class, - SecretClassVolumeProvisionParts::PublicPrivate, - ) - .build() - .context(TlsCertSecretClassVolumeBuildSnafu)?, - ) - .build(); - - pod_builder - .add_volume(opa_tls_volume) - .context(AddVolumeSnafu)?; - } - - // fault tolerant execution S3 credentials and other resources - if let Some(resolved_fte) = resolved_fte_config { - cb_prepare - .add_volume_mounts(resolved_fte.volume_mounts.clone()) - .context(AddVolumeMountSnafu)?; - cb_trino - .add_volume_mounts(resolved_fte.volume_mounts.clone()) - .context(AddVolumeMountSnafu)?; - pod_builder - .add_volumes(resolved_fte.volumes.clone()) - .context(AddVolumeSnafu)?; - } - - // client spooling S3 credentials and other resources - if let Some(resolved_spooling) = resolved_spooling_config { - cb_prepare - .add_volume_mounts(resolved_spooling.volume_mounts.clone()) - .context(AddVolumeMountSnafu)?; - cb_trino - .add_volume_mounts(resolved_spooling.volume_mounts.clone()) - .context(AddVolumeMountSnafu)?; - pod_builder - .add_volumes(resolved_spooling.volumes.clone()) - .context(AddVolumeSnafu)?; - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use stackable_operator::{ - commons::networking::DomainName, - kube::runtime::reflector::ObjectRef, - product_config_utils::{ - transform_all_roles_to_config, validate_all_roles_and_groups_config, - }, - }; - - use super::*; - use crate::{ - authentication::TrinoAuthenticationTypes, - config::{ - client_protocol::ResolvedClientProtocolConfig, - fault_tolerant_execution::ResolvedFaultTolerantExecutionConfig, - }, - crd::v1alpha1::TrinoCluster, - }; - - #[tokio::test] - async fn test_config_overrides() { - let trino_yaml = r#" - apiVersion: trino.stackable.tech/v1alpha1 - kind: TrinoCluster - metadata: - name: simple-trino - spec: - image: - productVersion: "479" - clusterConfig: - catalogLabelSelector: - matchLabels: - trino: simple-trino - coordinators: - configOverrides: - config.properties: - foo: bar - level: role - hello-from-role: "true" - internal-communication.https.keystore.path: /my/custom/internal-truststore.p12 - roleGroups: - default: - configOverrides: - config.properties: - foo: bar - level: role-group - hello-from-role-group: "true" - http-server.https.truststore.path: /my/custom/truststore.p12 - replicas: 1 - workers: - roleGroups: - default: - replicas: 1 - "#; - let cm = build_config_map(trino_yaml).await.data.unwrap(); - let config = cm.get("config.properties").unwrap(); - assert!(config.contains("foo=bar")); - assert!(config.contains("level=role-group")); - assert!(config.contains("hello-from-role=true")); - assert!(config.contains("hello-from-role-group=true")); - assert!(config.contains("http-server.https.enabled=true")); - assert!( - config.contains("http-server.https.keystore.path=/stackable/server_tls/keystore.p12") - ); - assert!(config.contains( - "internal-communication.https.keystore.path=/my/custom/internal-truststore.p12" - )); - // Overwritten by configOverrides from role (does work) - assert!(config.contains("http-server.https.truststore.path=/my/custom/truststore.p12")); - - assert!(cm.contains_key("jvm.config")); - assert!(cm.contains_key("security.properties")); - assert!(cm.contains_key("node.properties")); - assert!(cm.contains_key("log.properties")); - assert!(cm.contains_key("access-control.properties")); - } - - #[tokio::test] - async fn test_client_protocol_config_overrides() { - let trino_yaml = r#" - apiVersion: trino.stackable.tech/v1alpha1 - kind: TrinoCluster - metadata: - name: simple-trino - spec: - image: - productVersion: "479" - clusterConfig: - catalogLabelSelector: - matchLabels: - trino: simple-trino - clientProtocol: - spooling: - location: s3://my-bucket/spooling - filesystem: - s3: - connection: - reference: test-s3-connection - coordinators: - configOverrides: - config.properties: - foo: bar - spooling-manager.properties: - fs.location: s3a://role-level - roleGroups: - default: - replicas: 1 - configOverrides: - spooling-manager.properties: - fs.location: s3a://role-group-level - workers: - roleGroups: - default: - replicas: 1 - "#; - - let cm = build_config_map(trino_yaml).await.data.unwrap(); - let config = cm.get("config.properties").unwrap(); - assert!(config.contains("protocol.spooling.enabled=true")); - assert!(config.contains(&format!( - "protocol.spooling.shared-secret-key=${{ENV\\:{ENV_SPOOLING_SECRET}}}" - ))); - assert!(config.contains("foo=bar")); - - let config = cm.get("spooling-manager.properties").unwrap(); - assert!(config.contains("fs.location=s3a\\://role-group-level")); - assert!(config.contains("spooling-manager.name=filesystem")); - } - - async fn build_config_map(trino_yaml: &str) -> ConfigMap { - let deserializer = serde_yaml::Deserializer::from_str(trino_yaml); - let mut trino: TrinoCluster = - serde_yaml::with::singleton_map_recursive::deserialize(deserializer) - .expect("invalid test input"); - trino.metadata.namespace = Some("default".to_owned()); - trino.metadata.uid = Some("42".to_owned()); - let cluster_info = KubernetesClusterInfo { - cluster_domain: DomainName::try_from("cluster.local").unwrap(), - }; - let resolved_product_image = trino - .spec - .image - .resolve(CONTAINER_IMAGE_BASE_NAME, "oci.example.org", "0.0.0-dev") - .expect("test resolved product image is always valid"); - - let config_files = vec![ - PropertyNameKind::File(CONFIG_PROPERTIES.to_string()), - PropertyNameKind::File(NODE_PROPERTIES.to_string()), - PropertyNameKind::File(JVM_CONFIG.to_string()), - PropertyNameKind::File(LOG_PROPERTIES.to_string()), - PropertyNameKind::File(JVM_SECURITY_PROPERTIES.to_string()), - PropertyNameKind::File(ACCESS_CONTROL_PROPERTIES.to_string()), - PropertyNameKind::File(SPOOLING_MANAGER_PROPERTIES.to_string()), - PropertyNameKind::File(EXCHANGE_MANAGER_PROPERTIES.to_string()), - ]; - let validated_config = validate_all_roles_and_groups_config( - // The Trino version is a single number like 396. - // The product config expects semver formatted version strings. - // That is why we just add minor and patch version 0 here. - &format!("{}.0.0", resolved_product_image.product_version), - &transform_all_roles_to_config( - &trino, - &HashMap::from([ - ( - TrinoRole::Coordinator.to_string(), - ( - config_files.clone(), - trino.role(&TrinoRole::Coordinator).unwrap(), - ), - ), - ( - TrinoRole::Worker.to_string(), - (config_files, trino.role(&TrinoRole::Worker).unwrap()), - ), - ]), - ) - .unwrap(), - // Using this instead of ProductConfigManager::from_yaml_file, as that did not find the file - &ProductConfigManager::from_str(include_str!( - "../../../deploy/config-spec/properties.yaml" - )) - .unwrap(), - false, - false, - ) - .unwrap(); - - let trino_role = TrinoRole::Coordinator; - let role = trino.role(&trino_role).unwrap(); - let rolegroup_ref = RoleGroupRef { - cluster: ObjectRef::from_obj(&trino), - role: trino_role.to_string(), - role_group: "default".to_string(), - }; - let trino_authentication_config = TrinoAuthenticationConfig::new( - &resolved_product_image, - TrinoAuthenticationTypes::try_from(Vec::new()).unwrap(), - ) - .unwrap(); - let trino_opa_config = Some(TrinoOpaConfig { - non_batched_connection_string: - "http://simple-opa.default.svc.cluster.local:8081/v1/data/my-product/allow" - .to_string(), - batched_connection_string: - "http://simple-opa.default.svc.cluster.local:8081/v1/data/my-product/batch" - .to_string(), - row_filters_connection_string: Some( - "http://simple-opa.default.svc.cluster.local:8081/v1/data/my-product/rowFilters" - .to_string(), - ), - batched_column_masking_connection_string: Some( - "http://simple-opa.default.svc.cluster.local:8081/v1/data/my-product/batchColumnMasks" - .to_string(), - ), - allow_permission_management_operations: true, - tls_secret_class: None, - }); - let resolved_fte_config = match &trino.spec.cluster_config.fault_tolerant_execution { - Some(fault_tolerant_execution) => Some( - ResolvedFaultTolerantExecutionConfig::from_config( - fault_tolerant_execution, - None, - &trino.namespace().unwrap(), - ) - .await - .unwrap(), - ), - None => None, - }; - let resolved_spooling_config = match &trino.spec.cluster_config.client_protocol { - Some(client_protocol) => Some( - ResolvedClientProtocolConfig::from_config( - client_protocol, - None, - &trino.namespace().unwrap(), - ) - .await - .unwrap(), - ), - None => None, - }; - let merged_config = trino - .merged_config(&trino_role, &rolegroup_ref, &[]) - .unwrap(); - - build_rolegroup_config_map( - &trino, - &resolved_product_image, - &role, - &trino_role, - &rolegroup_ref, - validated_config - .get("coordinator") - .unwrap() - .get("default") - .unwrap(), - &merged_config, - &trino_authentication_config, - &trino_opa_config, - &cluster_info, - &resolved_fte_config, - &resolved_spooling_config, - ) - .unwrap() - } - - #[tokio::test] - async fn test_access_control_overrides() { - let trino_yaml = r#" - apiVersion: trino.stackable.tech/v1alpha1 - kind: TrinoCluster - metadata: - name: trino - spec: - image: - productVersion: "479" - clusterConfig: - catalogLabelSelector: - matchLabels: - trino: simple-trino - authorization: - opa: - configMapName: simple-opa - package: my-product - coordinators: - configOverrides: - access-control.properties: - hello-from-role: "true" # only defined here at role level - foo.bar: "false" # overridden by role group below - opa.allow-permission-management-operations: "false" # override value from config - roleGroups: - default: - configOverrides: - access-control.properties: - hello-from-role-group: "true" # only defined here at group level - foo.bar: "true" # overrides role value - opa.policy.batched-uri: "http://simple-opa.default.svc.cluster.local:8081/v1/data/my-product/batch-new" # override value from config - opa.policy.batch-column-masking-uri: "http://simple-opa.default.svc.cluster.local:8081/v1/data/my-product/batchColumnMasks-new" # override value from config - replicas: 1 - workers: - roleGroups: - default: - replicas: 1 - "#; - - let cm = build_config_map(trino_yaml).await.data.unwrap(); - let access_control_config = cm.get("access-control.properties").unwrap(); - - assert!(access_control_config.contains("access-control.name=opa")); - assert!(access_control_config.contains("hello-from-role=true")); - assert!(access_control_config.contains("hello-from-role-group=true")); - assert!(access_control_config.contains("foo.bar=true")); - assert!(access_control_config.contains("opa.allow-permission-management-operations=false")); - assert!(access_control_config.contains(r#"opa.policy.batched-uri=http\://simple-opa.default.svc.cluster.local\:8081/v1/data/my-product/batch-new"#)); - assert!(access_control_config.contains(r#"opa.policy.batch-column-masking-uri=http\://simple-opa.default.svc.cluster.local\:8081/v1/data/my-product/batchColumnMasks-new"#)); - assert!(access_control_config.contains(r#"opa.policy.row-filters-uri=http\://simple-opa.default.svc.cluster.local\:8081/v1/data/my-product/rowFilters"#)); - assert!(access_control_config.contains(r#"opa.policy.uri=http\://simple-opa.default.svc.cluster.local\:8081/v1/data/my-product/allow"#)); - } - - #[test] - fn test_env_overrides() { - let trino_yaml = r#" - apiVersion: trino.stackable.tech/v1alpha1 - kind: TrinoCluster - metadata: - name: trino - spec: - image: - productVersion: "479" - clusterConfig: - catalogLabelSelector: - matchLabels: - trino: simple-trino - coordinators: - envOverrides: - COMMON_VAR: role-value # overridden by role group below - ROLE_VAR: role-value # only defined here at role level - roleGroups: - default: - envOverrides: - COMMON_VAR: group-value # overrides role value - GROUP_VAR: group-value # only defined here at group level - replicas: 1 - workers: - roleGroups: - default: - replicas: 1 - "#; - let deserializer = serde_yaml::Deserializer::from_str(trino_yaml); - let trino: v1alpha1::TrinoCluster = - serde_yaml::with::singleton_map_recursive::deserialize(deserializer).unwrap(); - - let validated_config = validate::validated_product_config( - &trino, - "455.0.0", - &ProductConfigManager::from_yaml_file("../../deploy/config-spec/properties.yaml") - .unwrap(), - ) - .unwrap(); - - let env = validated_config - .get(&TrinoRole::Coordinator.to_string()) - .unwrap() - .get("default") - .unwrap() - .get(&PropertyNameKind::Env) - .unwrap(); - - assert_eq!(&"group-value".to_string(), env.get("COMMON_VAR").unwrap()); - assert_eq!(&"group-value".to_string(), env.get("GROUP_VAR").unwrap()); - assert_eq!(&"role-value".to_string(), env.get("ROLE_VAR").unwrap()); - } -} diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs new file mode 100644 index 000000000..6363166bb --- /dev/null +++ b/rust/operator-binary/src/controller/build.rs @@ -0,0 +1,16 @@ +//! Builders that turn a `ValidatedCluster` into Kubernetes resource contents. + +use std::str::FromStr; + +use stackable_operator::v2::types::operator::RoleGroupName; + +pub mod command; +pub mod graceful_shutdown; +pub mod ports; +pub mod properties; +pub mod resource; + +// Placeholder role-group name used for the recommended labels of a role's group listener. +// The group listener is owned by the role (not a single role-group), so there is no real +// role-group to attribute it to. +stackable_operator::constant!(pub(crate) PLACEHOLDER_LISTENER_ROLE_GROUP: RoleGroupName = "none"); diff --git a/rust/operator-binary/src/command.rs b/rust/operator-binary/src/controller/build/command.rs similarity index 81% rename from rust/operator-binary/src/command.rs rename to rust/operator-binary/src/controller/build/command.rs index e14cac173..5c20ee65c 100644 --- a/rust/operator-binary/src/command.rs +++ b/rust/operator-binary/src/controller/build/command.rs @@ -1,46 +1,44 @@ use stackable_operator::{ - product_logging::{ - framework::{create_vector_shutdown_file_command, remove_vector_shutdown_file_command}, - spec::{ContainerLogConfig, ContainerLogConfigChoice}, + product_logging::framework::{ + create_vector_shutdown_file_command, remove_vector_shutdown_file_command, }, utils::COMMON_BASH_TRAP_FUNCTIONS, + v2::product_logging::framework::ValidatedContainerLogConfigChoice, }; use crate::{ authentication::TrinoAuthenticationConfig, catalog::config::CatalogConfig, config::{client_protocol, fault_tolerant_execution}, - controller::{STACKABLE_LOG_CONFIG_DIR, STACKABLE_LOG_DIR}, + controller::{ValidatedCluster, ValidatedTrinoConfig, build::properties::ConfigFileName}, crd::{ - CONFIG_DIR_NAME, Container, EXCHANGE_MANAGER_PROPERTIES, LOG_PROPERTIES, - RW_CONFIG_DIR_NAME, SPOOLING_MANAGER_PROPERTIES, STACKABLE_CLIENT_TLS_DIR, + CONFIG_DIR_NAME, Container, RW_CONFIG_DIR_NAME, STACKABLE_CLIENT_TLS_DIR, STACKABLE_INTERNAL_TLS_DIR, STACKABLE_MOUNT_INTERNAL_TLS_DIR, STACKABLE_MOUNT_SERVER_TLS_DIR, STACKABLE_SERVER_TLS_DIR, STACKABLE_TLS_STORE_PASSWORD, - TrinoRole, v1alpha1, + TrinoRole, }, + trino_controller::{STACKABLE_LOG_CONFIG_DIR, STACKABLE_LOG_DIR}, }; pub fn container_prepare_args( - trino: &v1alpha1::TrinoCluster, + cluster: &ValidatedCluster, catalogs: &[CatalogConfig], - merged_config: &v1alpha1::TrinoConfig, + merged_config: &ValidatedTrinoConfig, resolved_fte_config: &Option, resolved_spooling_config: &Option, ) -> Vec { let mut args = vec![]; // Copy custom logging provided `log.properties` to rw config - if let Some(ContainerLogConfig { - choice: Some(ContainerLogConfigChoice::Custom(_)), - }) = merged_config.logging.containers.get(&Container::Trino) - { + if let ValidatedContainerLogConfigChoice::Custom(_) = merged_config.logging.trino_container { + let log_properties = ConfigFileName::Log; // copy config files to a writeable empty folder args.push(format!( - "echo copying {STACKABLE_LOG_CONFIG_DIR}/{LOG_PROPERTIES} {rw_conf}/{LOG_PROPERTIES}", + "echo copying {STACKABLE_LOG_CONFIG_DIR}/{log_properties} {rw_conf}/{log_properties}", rw_conf = RW_CONFIG_DIR_NAME )); args.push(format!( - "cp -RL {STACKABLE_LOG_CONFIG_DIR}/{LOG_PROPERTIES} {rw_conf}/{LOG_PROPERTIES}", + "cp -RL {STACKABLE_LOG_CONFIG_DIR}/{log_properties} {rw_conf}/{log_properties}", rw_conf = RW_CONFIG_DIR_NAME )); } @@ -51,15 +49,15 @@ pub fn container_prepare_args( // S3, LDAP, OIDC, FTE or whatnot. args.push(format!("cert-tools generate-pkcs12-truststore --pem /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem --out {STACKABLE_CLIENT_TLS_DIR}/truststore.p12 --out-password {STACKABLE_TLS_STORE_PASSWORD}")); - if trino.tls_enabled() { + if cluster.tls_enabled() { args.push(format!("cp {STACKABLE_MOUNT_SERVER_TLS_DIR}/truststore.p12 {STACKABLE_SERVER_TLS_DIR}/truststore.p12")); args.push(format!("cp {STACKABLE_MOUNT_SERVER_TLS_DIR}/keystore.p12 {STACKABLE_SERVER_TLS_DIR}/keystore.p12")); } - if trino.get_internal_tls().is_some() { + if cluster.get_internal_tls().is_some() { args.push(format!("cp {STACKABLE_MOUNT_INTERNAL_TLS_DIR}/truststore.p12 {STACKABLE_INTERNAL_TLS_DIR}/truststore.p12")); args.push(format!("cp {STACKABLE_MOUNT_INTERNAL_TLS_DIR}/keystore.p12 {STACKABLE_INTERNAL_TLS_DIR}/keystore.p12")); - if trino.tls_enabled() { + if cluster.tls_enabled() { args.push(format!("cert-tools generate-pkcs12-truststore --pkcs12 {STACKABLE_MOUNT_SERVER_TLS_DIR}/truststore.p12:{STACKABLE_TLS_STORE_PASSWORD} --pkcs12 {STACKABLE_INTERNAL_TLS_DIR}/truststore.p12:{STACKABLE_TLS_STORE_PASSWORD} --out {STACKABLE_INTERNAL_TLS_DIR}/truststore.p12 --out-password {STACKABLE_TLS_STORE_PASSWORD}")); } } @@ -115,13 +113,19 @@ pub fn container_trino_args( // Resolve credentials for fault tolerant execution exchange manager if needed args.push(format!( "test -f {rw_exchange_manager_config_file} && config-utils template {rw_exchange_manager_config_file}", - rw_exchange_manager_config_file = format!("{RW_CONFIG_DIR_NAME}/{EXCHANGE_MANAGER_PROPERTIES}") + rw_exchange_manager_config_file = format!( + "{RW_CONFIG_DIR_NAME}/{exchange_manager}", + exchange_manager = ConfigFileName::ExchangeManager + ) )); // Resolve credentials for spooling manager if needed args.push(format!( "test -f {rw_spooling_config_file} && config-utils template {rw_spooling_config_file}", - rw_spooling_config_file = format!("{RW_CONFIG_DIR_NAME}/{SPOOLING_MANAGER_PROPERTIES}") + rw_spooling_config_file = format!( + "{RW_CONFIG_DIR_NAME}/{spooling_manager}", + spooling_manager = ConfigFileName::SpoolingManager + ) )); args.push("set -x".to_string()); diff --git a/rust/operator-binary/src/controller/build/graceful_shutdown.rs b/rust/operator-binary/src/controller/build/graceful_shutdown.rs new file mode 100644 index 000000000..37a076204 --- /dev/null +++ b/rust/operator-binary/src/controller/build/graceful_shutdown.rs @@ -0,0 +1,367 @@ +//! See docs/modules/trino/pages/operations/graceful-shutdown.adoc for details +//! on how the implementation works +use std::collections::BTreeMap; + +use indoc::formatdoc; +use snafu::{ResultExt, Snafu}; +use stackable_operator::{ + builder::pod::{PodBuilder, container::ContainerBuilder}, + k8s_openapi::api::core::v1::{ExecAction, LifecycleHandler}, + shared::time::Duration, +}; + +use crate::{ + controller::{ValidatedCluster, ValidatedTrinoConfig}, + crd::{DEFAULT_WORKER_GRACEFUL_SHUTDOWN_TIMEOUT, TrinoRole}, +}; + +/// Corresponds to "shutdown.grace-period", which defaults to 2 min. +/// This seems a bit high, as Pod termination - even with no queries running on the worker - +/// takes at least 4 minutes (see ). +/// So we set it to 30 seconds, so the Pod termination takes at least 1 minute. +const WORKER_SHUTDOWN_GRACE_PERIOD: Duration = Duration::from_secs(30); + +/// Safety puffer to guarantee the graceful shutdown works every time. +const WORKER_GRACEFUL_SHUTDOWN_SAFETY_OVERHEAD: Duration = Duration::from_secs(10); + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("failed to set terminationGracePeriod"))] + SetTerminationGracePeriod { + source: stackable_operator::builder::pod::Error, + }, +} + +/// Computes the graceful-shutdown-related properties for the role's +/// `config.properties` file from a [`ValidatedCluster`]. +pub fn graceful_shutdown_config_properties( + cluster: &ValidatedCluster, + role: TrinoRole, +) -> BTreeMap { + match role { + TrinoRole::Coordinator => { + // Only set query.max-execution-time if fault tolerant execution is not configured. + // With fault tolerant execution enabled, queries can be retried and run indefinitely. + if cluster.cluster_config.fault_tolerant_execution.is_none() { + let min_worker_graceful_shutdown_timeout = + min_worker_graceful_shutdown_timeout(cluster); + // We know that queries taking longer than the minimum gracefulShutdownTimeout are subject to failure. + // Read operator docs for reasoning. + BTreeMap::from([( + "query.max-execution-time".to_string(), + format!("{}s", min_worker_graceful_shutdown_timeout.as_secs()), + )]) + } else { + BTreeMap::new() + } + } + TrinoRole::Worker => BTreeMap::from([( + "shutdown.grace-period".to_string(), + format!("{}s", WORKER_SHUTDOWN_GRACE_PERIOD.as_secs()), + )]), + } +} + +/// Returns the minimal `gracefulShutdownTimeout` across all worker role-groups, read from the +/// validated [`ValidatedCluster::role_group_configs`]. +fn min_worker_graceful_shutdown_timeout( + cluster: &ValidatedCluster, +) -> stackable_operator::shared::time::Duration { + cluster + .role_group_configs + .get(&TrinoRole::Worker) + .into_iter() + .flat_map(|groups| groups.values()) + .filter_map(|rg| rg.config.graceful_shutdown_timeout) + .min() + .unwrap_or(DEFAULT_WORKER_GRACEFUL_SHUTDOWN_TIMEOUT) +} + +pub fn add_graceful_shutdown_config( + cluster: &ValidatedCluster, + role: &TrinoRole, + merged_config: &ValidatedTrinoConfig, + pod_builder: &mut PodBuilder, + trino_builder: &mut ContainerBuilder, +) -> Result<(), Error> { + // This must be always set by the merge mechanism, as we provide a default value, + // users can not disable graceful shutdown. + if let Some(graceful_shutdown_timeout) = merged_config.graceful_shutdown_timeout { + match role { + TrinoRole::Coordinator => { + pod_builder + .termination_grace_period(&graceful_shutdown_timeout) + .context(SetTerminationGracePeriodSnafu)?; + } + TrinoRole::Worker => { + // We could stick `graceful_shutdown_timeout` into the Pod's `termination_grace_period_seconds` and subtract all the overheads + // from it and use that for `query.max-execution-time`. However, as `query.max-execution-time` is user-facing, we set to the configured + // `graceful_shutdown_timeout` and add the overhead to the Pod's `termination_grace_period_seconds`. + let termination_grace_period = graceful_shutdown_timeout + + 2 * WORKER_SHUTDOWN_GRACE_PERIOD + + WORKER_GRACEFUL_SHUTDOWN_SAFETY_OVERHEAD; + let termination_grace_period_seconds = termination_grace_period.as_secs(); + + pod_builder + .termination_grace_period(&termination_grace_period) + .context(SetTerminationGracePeriodSnafu)?; + trino_builder.lifecycle_pre_stop(LifecycleHandler { + exec: Some(ExecAction { + command: Some(vec![ + "/bin/bash".to_string(), + "-x".to_string(), + "-euo".to_string(), + "pipefail".to_string(), + "-c".to_string(), + // The curl does not block, but the worker process will terminate automatically once the + // graceful shutdown is complete. As the Pod gets a normal SIGTERM sent once the hook + // exited, we need to block this call for at least the same time terminationGracePeriodSeconds + // does, so that we don't kill the Pod before the terminationGracePeriodSeconds is reached. + + // FIXME: Once we have fully fledged OPA support we need to make sure that the user we choose here (e.g. admin) + // has the permissions to trigger a graceful shutdown by e.g. inserting the needed OPA rules transparently. + formatdoc!(" + curl -v --fail --insecure -X PUT -d '\"SHUTTING_DOWN\"' -H 'Content-type: application/json' -H 'X-Trino-User: graceful-shutdown-user' -H 'X-Trino-Source: Stackable data platform' {protocol}://{host}:{port}/v1/info/state >> /proc/1/fd/1 2>&1 + echo 'Successfully sent graceful shutdown command' >> /proc/1/fd/1 2>&1 + echo 'Sleeping {termination_grace_period_seconds} seconds' >> /proc/1/fd/1 2>&1 + sleep {termination_grace_period_seconds}", + protocol = super::ports::exposed_protocol(cluster), + host = "127.0.0.1", + port = super::ports::exposed_port(cluster), + ), + ]), + }), + ..Default::default() + }); + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use stackable_operator::shared::time::Duration; + + use super::*; + use crate::{ + config::fault_tolerant_execution::ResolvedFaultTolerantExecutionConfig, + controller::build::properties::test_support::{ + MINIMAL_TRINO_YAML, empty_derefs, validated_cluster_from_yaml, + validated_cluster_from_yaml_with_derefs, + }, + }; + + /// A worker role group without an explicit `gracefulShutdownTimeout` falls back to the + /// product default. + #[test] + fn min_worker_timeout_defaults() { + let cluster = validated_cluster_from_yaml( + r#" + apiVersion: trino.stackable.tech/v1alpha1 + kind: TrinoCluster + metadata: + name: simple-trino + namespace: default + uid: "e6ac237d-a6d4-43a1-8135-f36506110912" + spec: + image: + productVersion: "479" + clusterConfig: + catalogLabelSelector: {} + coordinators: + roleGroups: + default: + replicas: 1 + workers: + roleGroups: + default: + replicas: 1 + "#, + ); + assert_eq!( + min_worker_graceful_shutdown_timeout(&cluster), + DEFAULT_WORKER_GRACEFUL_SHUTDOWN_TIMEOUT + ); + } + + /// A role-level `gracefulShutdownTimeout` is merged into every worker role group. + #[test] + fn min_worker_timeout_from_role() { + let cluster = validated_cluster_from_yaml( + r#" + apiVersion: trino.stackable.tech/v1alpha1 + kind: TrinoCluster + metadata: + name: simple-trino + namespace: default + uid: "e6ac237d-a6d4-43a1-8135-f36506110912" + spec: + image: + productVersion: "479" + clusterConfig: + catalogLabelSelector: {} + coordinators: + roleGroups: + default: + replicas: 1 + workers: + config: + gracefulShutdownTimeout: 42h + roleGroups: + default: + replicas: 1 + "#, + ); + assert_eq!( + min_worker_graceful_shutdown_timeout(&cluster), + Duration::from_hours_unchecked(42) + ); + } + + /// The minimum is taken across all worker role groups (role <- role-group merge applied). + #[test] + fn min_worker_timeout_across_role_groups() { + let cluster = validated_cluster_from_yaml( + r#" + apiVersion: trino.stackable.tech/v1alpha1 + kind: TrinoCluster + metadata: + name: simple-trino + namespace: default + uid: "e6ac237d-a6d4-43a1-8135-f36506110912" + spec: + image: + productVersion: "479" + clusterConfig: + catalogLabelSelector: {} + coordinators: + roleGroups: + default: + replicas: 1 + workers: + config: + gracefulShutdownTimeout: 42h + roleGroups: + normal: + replicas: 1 + short: + replicas: 1 + config: + gracefulShutdownTimeout: 5m + long: + replicas: 1 + config: + gracefulShutdownTimeout: 7d + "#, + ); + assert_eq!( + min_worker_graceful_shutdown_timeout(&cluster), + Duration::from_minutes_unchecked(5) + ); + } + + fn fte_derefs() -> crate::controller::dereference::DereferencedObjects { + let mut derefs = empty_derefs(); + derefs.resolved_fte_config = Some(ResolvedFaultTolerantExecutionConfig { + config_properties: BTreeMap::new(), + exchange_manager_properties: BTreeMap::new(), + volumes: Vec::new(), + volume_mounts: Vec::new(), + init_container_extra_start_commands: Vec::new(), + }); + derefs + } + + #[test] + fn coordinator_props_set_query_max_execution_time_without_fte() { + let cluster = validated_cluster_from_yaml(MINIMAL_TRINO_YAML); + let props = graceful_shutdown_config_properties(&cluster, TrinoRole::Coordinator); + // The default worker graceful-shutdown timeout is 60 minutes (3600s). + assert_eq!( + props.get("query.max-execution-time").map(String::as_str), + Some("3600s") + ); + } + + #[test] + fn coordinator_props_empty_with_fault_tolerant_execution() { + let cluster = validated_cluster_from_yaml_with_derefs(MINIMAL_TRINO_YAML, fte_derefs()); + let props = graceful_shutdown_config_properties(&cluster, TrinoRole::Coordinator); + // With fault-tolerant execution, queries may be retried, so no max-execution-time is set. + assert!(props.is_empty()); + } + + #[test] + fn worker_props_set_shutdown_grace_period() { + let cluster = validated_cluster_from_yaml(MINIMAL_TRINO_YAML); + let props = graceful_shutdown_config_properties(&cluster, TrinoRole::Worker); + assert_eq!( + props.get("shutdown.grace-period").map(String::as_str), + Some("30s") + ); + } + + #[test] + fn worker_termination_grace_period_adds_overhead_and_sets_pre_stop() { + let cluster = validated_cluster_from_yaml(MINIMAL_TRINO_YAML); + let merged = &cluster.role_group_configs[&TrinoRole::Worker] + .values() + .next() + .unwrap() + .config; + let mut pod_builder = PodBuilder::new(); + let mut trino_builder = ContainerBuilder::new("trino").unwrap(); + add_graceful_shutdown_config( + &cluster, + &TrinoRole::Worker, + merged, + &mut pod_builder, + &mut trino_builder, + ) + .unwrap(); + + // Default worker timeout 3600s + 2 * 30s grace + 10s safety = 3670s. + let spec = pod_builder.build_template().spec.unwrap(); + assert_eq!(spec.termination_grace_period_seconds, Some(3670)); + + let command = trino_builder + .build() + .lifecycle + .unwrap() + .pre_stop + .unwrap() + .exec + .unwrap() + .command + .unwrap(); + assert!(command.iter().any(|arg| arg.contains("sleep 3670"))); + } + + #[test] + fn coordinator_termination_grace_period_has_no_overhead_or_pre_stop() { + let cluster = validated_cluster_from_yaml(MINIMAL_TRINO_YAML); + let merged = &cluster.role_group_configs[&TrinoRole::Coordinator] + .values() + .next() + .unwrap() + .config; + let mut pod_builder = PodBuilder::new(); + let mut trino_builder = ContainerBuilder::new("trino").unwrap(); + add_graceful_shutdown_config( + &cluster, + &TrinoRole::Coordinator, + merged, + &mut pod_builder, + &mut trino_builder, + ) + .unwrap(); + + // The coordinator default timeout (900s) is used verbatim, with no overhead. + let spec = pod_builder.build_template().spec.unwrap(); + assert_eq!(spec.termination_grace_period_seconds, Some(900)); + // Coordinators do not get a graceful-shutdown pre-stop hook. + assert!(trino_builder.build().lifecycle.is_none()); + } +} diff --git a/rust/operator-binary/src/controller/build/ports.rs b/rust/operator-binary/src/controller/build/ports.rs new file mode 100644 index 000000000..a681c8a0a --- /dev/null +++ b/rust/operator-binary/src/controller/build/ports.rs @@ -0,0 +1,29 @@ +//! The client-facing port Trino exposes, derived from the validated TLS configuration. +//! +//! Mapping the server-TLS flag onto a concrete port number / name is a resource-shaping decision, +//! so it lives in the build step rather than on [`ValidatedCluster`]. + +use stackable_operator::v2::types::common::Port; + +use crate::{ + controller::ValidatedCluster, + crd::{HTTP_PORT, HTTP_PORT_NAME, HTTPS_PORT, HTTPS_PORT_NAME}, +}; + +/// The client-facing port Trino exposes: HTTPS when server TLS is enabled, otherwise HTTP. +pub(crate) fn exposed_port(cluster: &ValidatedCluster) -> Port { + if cluster.server_tls_enabled() { + HTTPS_PORT + } else { + HTTP_PORT + } +} + +/// The name of the client-facing port (see [`exposed_port`]). +pub(crate) fn exposed_protocol(cluster: &ValidatedCluster) -> &'static str { + if cluster.server_tls_enabled() { + HTTPS_PORT_NAME + } else { + HTTP_PORT_NAME + } +} diff --git a/rust/operator-binary/src/controller/build/properties/access_control_properties.rs b/rust/operator-binary/src/controller/build/properties/access_control_properties.rs new file mode 100644 index 000000000..0c7e47b8d --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/access_control_properties.rs @@ -0,0 +1,48 @@ +//! Builder for `access-control.properties`. + +use std::collections::BTreeMap; + +use crate::controller::{TrinoRoleGroupConfig, ValidatedCluster}; + +/// Build the `access-control.properties` key/value pairs. +/// +/// Returns an empty map when neither OPA authorization is configured nor user overrides are provided. +/// Callers should omit the file from the ConfigMap in that case. +pub fn build(cluster: &ValidatedCluster, rg: &TrinoRoleGroupConfig) -> BTreeMap { + let mut props = BTreeMap::new(); + + // 1. No defaults. + // 2. Automatic OPA config when configured. + if let Some(opa) = &cluster.cluster_config.authorization { + props.extend(opa.as_config()); + } + + // 3. No merged_config contribution. + // 4. User overrides (highest precedence). + props.extend(rg.config_overrides.access_control_properties.clone()); + + props +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + controller::build::properties::test_support::{ + MINIMAL_TRINO_YAML, validated_cluster_from_yaml, + }, + crd::TrinoRole, + }; + + #[test] + fn default_renders_empty_when_no_opa() { + let cluster = validated_cluster_from_yaml(MINIMAL_TRINO_YAML); + let rg = cluster.role_group_configs[&TrinoRole::Coordinator] + .values() + .next() + .unwrap() + .clone(); + let props = build(&cluster, &rg); + assert!(props.is_empty()); + } +} diff --git a/rust/operator-binary/src/controller/build/properties/config_properties.rs b/rust/operator-binary/src/controller/build/properties/config_properties.rs new file mode 100644 index 000000000..baea16cee --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/config_properties.rs @@ -0,0 +1,470 @@ +//! Builder for `config.properties`. The main Trino server config. + +use std::{collections::BTreeMap, ops::Div}; + +use snafu::Snafu; +use stackable_operator::{memory::BinaryMultiple, utils::cluster_info::KubernetesClusterInfo}; + +use crate::{ + controller::{TrinoRoleGroupConfig, ValidatedCluster}, + crd::{ + Container, ENV_INTERNAL_SECRET, HTTP_PORT, HTTPS_PORT, MAX_TRINO_LOG_FILES_SIZE, + STACKABLE_INTERNAL_TLS_DIR, STACKABLE_LOG_DIR, STACKABLE_SERVER_TLS_DIR, + STACKABLE_TLS_STORE_PASSWORD, TrinoRole, + discovery::{TrinoDiscovery, TrinoDiscoveryProtocol}, + }, +}; + +const NODE_SCHEDULER_INCLUDE_COORDINATOR: &str = "node-scheduler.include-coordinator"; +const HTTP_SERVER_LOG_ENABLED: &str = "http-server.log.enabled"; + +// config.properties +const COORDINATOR: &str = "coordinator"; +const DISCOVERY_URI: &str = "discovery.uri"; +const HTTP_SERVER_HTTP_PORT: &str = "http-server.http.port"; +const QUERY_MAX_MEMORY: &str = "query.max-memory"; +const QUERY_MAX_MEMORY_PER_NODE: &str = "query.max-memory-per-node"; +// - server tls +const HTTP_SERVER_HTTPS_PORT: &str = "http-server.https.port"; +const HTTP_SERVER_HTTPS_ENABLED: &str = "http-server.https.enabled"; +const HTTP_SERVER_HTTPS_KEYSTORE_KEY: &str = "http-server.https.keystore.key"; +const HTTP_SERVER_KEYSTORE_PATH: &str = "http-server.https.keystore.path"; +const HTTP_SERVER_HTTPS_TRUSTSTORE_KEY: &str = "http-server.https.truststore.key"; +const HTTP_SERVER_TRUSTSTORE_PATH: &str = "http-server.https.truststore.path"; +const HTTP_SERVER_AUTHENTICATION_ALLOW_INSECURE_OVER_HTTP: &str = + "http-server.authentication.allow-insecure-over-http"; +// - internal tls +const INTERNAL_COMMUNICATION_SHARED_SECRET: &str = "internal-communication.shared-secret"; +const INTERNAL_COMMUNICATION_HTTPS_KEYSTORE_PATH: &str = + "internal-communication.https.keystore.path"; +const INTERNAL_COMMUNICATION_HTTPS_KEYSTORE_KEY: &str = "internal-communication.https.keystore.key"; +const INTERNAL_COMMUNICATION_HTTPS_TRUSTSTORE_PATH: &str = + "internal-communication.https.truststore.path"; +const INTERNAL_COMMUNICATION_HTTPS_TRUSTSTORE_KEY: &str = + "internal-communication.https.truststore.key"; +const NODE_INTERNAL_ADDRESS_SOURCE: &str = "node.internal-address-source"; +const NODE_INTERNAL_ADDRESS_SOURCE_FQDN: &str = "FQDN"; +// Logging +const LOG_FORMAT: &str = "log.format"; +const LOG_PATH: &str = "log.path"; +const LOG_COMPRESSION: &str = "log.compression"; +const LOG_MAX_SIZE: &str = "log.max-size"; +const LOG_MAX_TOTAL_SIZE: &str = "log.max-total-size"; + +// Default values for `config.properties`. +const DEFAULT_QUERY_MAX_MEMORY: &str = "50GB"; +const DEFAULT_NODE_SCHEDULER_INCLUDE_COORDINATOR: &str = "false"; + +const LOG_FILE_COUNT: u32 = 2; + +// TLS keystore/truststore file names (PKCS#12). +const KEYSTORE_P12: &str = "keystore.p12"; +const TRUSTSTORE_P12: &str = "truststore.p12"; + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display( + "Trino requires client TLS to be enabled if any authentication method is enabled" + ))] + AuthenticationRequiresTls, +} + +/// Build the `config.properties` key/value pairs. +pub fn build( + cluster: &ValidatedCluster, + role: TrinoRole, + rg: &TrinoRoleGroupConfig, + cluster_info: &KubernetesClusterInfo, +) -> Result, Error> { + let mut props = BTreeMap::new(); + + // ---- 1. Hardcoded defaults (lowest precedence) ---- + props.insert( + QUERY_MAX_MEMORY.to_string(), + DEFAULT_QUERY_MAX_MEMORY.to_string(), + ); + if role == TrinoRole::Coordinator { + props.insert( + NODE_SCHEDULER_INCLUDE_COORDINATOR.to_string(), + DEFAULT_NODE_SCHEDULER_INCLUDE_COORDINATOR.to_string(), + ); + } + + // ---- 2. Operator-injected automatic values ---- + props.insert( + COORDINATOR.to_string(), + (role == TrinoRole::Coordinator).to_string(), + ); + + // Trino's own JSON logging output. + props.insert(LOG_FORMAT.to_string(), "json".to_string()); + props.insert( + LOG_PATH.to_string(), + format!( + "{STACKABLE_LOG_DIR}/{container}/server.airlift.json", + container = Container::Trino + ), + ); + props.insert(LOG_COMPRESSION.to_string(), "none".to_string()); + props.insert( + LOG_MAX_SIZE.to_string(), + format!( + // Trino uses the unit "MB" for MiB. + "{}MB", + MAX_TRINO_LOG_FILES_SIZE + .scale_to(BinaryMultiple::Mebi) + .div(LOG_FILE_COUNT as f32) + .ceil() + .value, + ), + ); + props.insert( + LOG_MAX_TOTAL_SIZE.to_string(), + format!( + "{}MB", + MAX_TRINO_LOG_FILES_SIZE + .scale_to(BinaryMultiple::Mebi) + .ceil() + .value, + ), + ); + props.insert(HTTP_SERVER_LOG_ENABLED.to_string(), "false".to_string()); + props.insert( + INTERNAL_COMMUNICATION_SHARED_SECRET.to_string(), + format!("${{ENV:{ENV_INTERNAL_SECRET}}}"), + ); + + // TLS gating, including the authentication-requires-TLS check. + let server_tls_enabled = cluster.server_tls_enabled(); + let internal_tls_enabled = cluster.internal_tls_enabled(); + if cluster.cluster_config.authentication_enabled() && !server_tls_enabled { + return Err(Error::AuthenticationRequiresTls); + } + if server_tls_enabled || internal_tls_enabled { + props.insert(HTTP_SERVER_HTTPS_ENABLED.to_string(), "true".to_string()); + props.insert(HTTP_SERVER_HTTPS_PORT.to_string(), HTTPS_PORT.to_string()); + let tls_store_dir = if server_tls_enabled { + STACKABLE_SERVER_TLS_DIR + } else { + // allow insecure communication via the http port + props.insert( + HTTP_SERVER_AUTHENTICATION_ALLOW_INSECURE_OVER_HTTP.to_string(), + "true".to_string(), + ); + props.insert(HTTP_SERVER_HTTP_PORT.to_string(), HTTP_PORT.to_string()); + STACKABLE_INTERNAL_TLS_DIR + }; + props.insert( + HTTP_SERVER_KEYSTORE_PATH.to_string(), + format!("{tls_store_dir}/{KEYSTORE_P12}"), + ); + props.insert( + HTTP_SERVER_HTTPS_KEYSTORE_KEY.to_string(), + STACKABLE_TLS_STORE_PASSWORD.to_string(), + ); + props.insert( + HTTP_SERVER_TRUSTSTORE_PATH.to_string(), + format!("{tls_store_dir}/{TRUSTSTORE_P12}"), + ); + props.insert( + HTTP_SERVER_HTTPS_TRUSTSTORE_KEY.to_string(), + STACKABLE_TLS_STORE_PASSWORD.to_string(), + ); + } + if internal_tls_enabled { + props.insert( + INTERNAL_COMMUNICATION_HTTPS_KEYSTORE_PATH.to_string(), + format!("{STACKABLE_INTERNAL_TLS_DIR}/{KEYSTORE_P12}"), + ); + props.insert( + INTERNAL_COMMUNICATION_HTTPS_KEYSTORE_KEY.to_string(), + STACKABLE_TLS_STORE_PASSWORD.to_string(), + ); + props.insert( + INTERNAL_COMMUNICATION_HTTPS_TRUSTSTORE_PATH.to_string(), + format!("{STACKABLE_INTERNAL_TLS_DIR}/{TRUSTSTORE_P12}"), + ); + props.insert( + INTERNAL_COMMUNICATION_HTTPS_TRUSTSTORE_KEY.to_string(), + STACKABLE_TLS_STORE_PASSWORD.to_string(), + ); + props.insert( + NODE_INTERNAL_ADDRESS_SOURCE.to_string(), + NODE_INTERNAL_ADDRESS_SOURCE_FQDN.to_string(), + ); + } + + // Authentication properties (only contributes when authentication is enabled). + props.extend( + cluster + .cluster_config + .authentication + .config_properties(&role), + ); + + // Discovery URI. + if let Some(coordinator_ref) = cluster.cluster_config.coordinator_pod_refs.first() { + let protocol = if internal_tls_enabled { + TrinoDiscoveryProtocol::Https + } else { + TrinoDiscoveryProtocol::Http + }; + let discovery = TrinoDiscovery::new(coordinator_ref, protocol); + props.insert( + DISCOVERY_URI.to_string(), + discovery.discovery_uri(cluster_info), + ); + } + + // Graceful shutdown. + props.extend( + crate::controller::build::graceful_shutdown::graceful_shutdown_config_properties( + cluster, role, + ), + ); + + // Fault-tolerant execution. + if let Some(fte) = &cluster.cluster_config.fault_tolerant_execution { + props.extend(fte.config_properties.clone()); + } + // Client spooling protocol. + if let Some(spooling) = &cluster.cluster_config.client_protocol { + props.extend(spooling.config_properties.clone()); + } + + // ---- 3. merged_config CRD-spec values ---- + if let Some(qmm) = &rg.config.query_max_memory { + props.insert(QUERY_MAX_MEMORY.to_string(), qmm.clone()); + } + if let Some(qmmpn) = &rg.config.query_max_memory_per_node { + props.insert(QUERY_MAX_MEMORY_PER_NODE.to_string(), qmmpn.clone()); + } + + // ---- 4. User overrides (highest precedence) ---- + props.extend(rg.config_overrides.config_properties.clone()); + + Ok(props) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::controller::build::properties::test_support::{ + MINIMAL_TRINO_YAML, file_auth_class, validated_cluster_from_yaml, + validated_cluster_from_yaml_with_auth, + }; + + const SERVER_TLS_ONLY_YAML: &str = r#" + apiVersion: trino.stackable.tech/v1alpha1 + kind: TrinoCluster + metadata: + name: simple-trino + namespace: default + uid: "e6ac237d-a6d4-43a1-8135-f36506110912" + spec: + image: + productVersion: "479" + clusterConfig: + catalogLabelSelector: {} + tls: + internalSecretClass: null + coordinators: + roleGroups: + default: + replicas: 1 + workers: + roleGroups: + default: + replicas: 1 + "#; + + const INTERNAL_TLS_ONLY_YAML: &str = r#" + apiVersion: trino.stackable.tech/v1alpha1 + kind: TrinoCluster + metadata: + name: simple-trino + namespace: default + uid: "e6ac237d-a6d4-43a1-8135-f36506110912" + spec: + image: + productVersion: "479" + clusterConfig: + catalogLabelSelector: {} + tls: + serverSecretClass: null + coordinators: + roleGroups: + default: + replicas: 1 + workers: + roleGroups: + default: + replicas: 1 + "#; + + fn cluster_info() -> stackable_operator::utils::cluster_info::KubernetesClusterInfo { + stackable_operator::utils::cluster_info::KubernetesClusterInfo { + cluster_domain: stackable_operator::commons::networking::DomainName::try_from( + "cluster.local", + ) + .unwrap(), + } + } + + fn rg(cluster: &ValidatedCluster, role: &TrinoRole) -> TrinoRoleGroupConfig { + cluster.role_group_configs[role] + .values() + .next() + .expect("the fixture defines a role group") + .clone() + } + + #[test] + fn default_renders_includes_coordinator_default_and_query_max_memory_default() { + let cluster = validated_cluster_from_yaml(MINIMAL_TRINO_YAML); + let rg = cluster.role_group_configs[&TrinoRole::Coordinator] + .values() + .next() + .unwrap() + .clone(); + let cluster_info = stackable_operator::utils::cluster_info::KubernetesClusterInfo { + cluster_domain: stackable_operator::commons::networking::DomainName::try_from( + "cluster.local", + ) + .unwrap(), + }; + let props = build(&cluster, TrinoRole::Coordinator, &rg, &cluster_info).unwrap(); + assert_eq!(props.get("coordinator").map(String::as_str), Some("true")); + assert_eq!( + props + .get("node-scheduler.include-coordinator") + .map(String::as_str), + Some("false"), + ); + assert_eq!( + props.get("query.max-memory").map(String::as_str), + Some("50GB") + ); + } + + #[test] + fn server_tls_only_uses_server_keystore_dir_and_http_discovery() { + let cluster = validated_cluster_from_yaml(SERVER_TLS_ONLY_YAML); + let props = build( + &cluster, + TrinoRole::Coordinator, + &rg(&cluster, &TrinoRole::Coordinator), + &cluster_info(), + ) + .unwrap(); + + assert_eq!( + props.get("http-server.https.enabled").map(String::as_str), + Some("true") + ); + assert_eq!( + props.get("http-server.https.port").map(String::as_str), + Some("8443") + ); + assert_eq!( + props + .get("http-server.https.keystore.path") + .map(String::as_str), + Some("/stackable/server_tls/keystore.p12"), + ); + // Server TLS is on, so insecure HTTP access must not be allowed. + assert_eq!( + props.get("http-server.authentication.allow-insecure-over-http"), + None + ); + // Internal TLS is off: no internal-communication keystore or FQDN address source. + assert_eq!( + props.get("internal-communication.https.keystore.path"), + None + ); + assert_eq!(props.get("node.internal-address-source"), None); + // Discovery uses http when internal TLS is disabled. + assert!(props.get("discovery.uri").unwrap().starts_with("http://")); + } + + #[test] + fn internal_tls_only_allows_insecure_http_and_uses_internal_keystore_dir() { + let cluster = validated_cluster_from_yaml(INTERNAL_TLS_ONLY_YAML); + let props = build( + &cluster, + TrinoRole::Coordinator, + &rg(&cluster, &TrinoRole::Coordinator), + &cluster_info(), + ) + .unwrap(); + + assert_eq!( + props.get("http-server.https.enabled").map(String::as_str), + Some("true") + ); + assert_eq!( + props + .get("http-server.authentication.allow-insecure-over-http") + .map(String::as_str), + Some("true"), + ); + assert_eq!( + props.get("http-server.http.port").map(String::as_str), + Some("8080") + ); + assert_eq!( + props + .get("http-server.https.keystore.path") + .map(String::as_str), + Some("/stackable/internal_tls/keystore.p12"), + ); + // Internal TLS block is present. + assert_eq!( + props + .get("internal-communication.https.keystore.path") + .map(String::as_str), + Some("/stackable/internal_tls/keystore.p12"), + ); + assert_eq!( + props + .get("node.internal-address-source") + .map(String::as_str), + Some("FQDN") + ); + // Discovery uses https when internal TLS is enabled. + assert!(props.get("discovery.uri").unwrap().starts_with("https://")); + } + + #[test] + fn worker_omits_include_coordinator() { + let cluster = validated_cluster_from_yaml(MINIMAL_TRINO_YAML); + let props = build( + &cluster, + TrinoRole::Worker, + &rg(&cluster, &TrinoRole::Worker), + &cluster_info(), + ) + .unwrap(); + + assert_eq!(props.get("coordinator").map(String::as_str), Some("false")); + assert_eq!(props.get("node-scheduler.include-coordinator"), None); + } + + #[test] + fn authentication_without_server_tls_errors() { + // Server TLS off (only internal) plus an enabled authenticator must be rejected. + let cluster = validated_cluster_from_yaml_with_auth( + INTERNAL_TLS_ONLY_YAML, + vec![file_auth_class("file-auth")], + ); + let err = build( + &cluster, + TrinoRole::Coordinator, + &rg(&cluster, &TrinoRole::Coordinator), + &cluster_info(), + ) + .unwrap_err(); + + assert!(matches!(err, Error::AuthenticationRequiresTls)); + } +} diff --git a/rust/operator-binary/src/controller/build/properties/exchange_manager_properties.rs b/rust/operator-binary/src/controller/build/properties/exchange_manager_properties.rs new file mode 100644 index 000000000..337e21751 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/exchange_manager_properties.rs @@ -0,0 +1,48 @@ +//! Builder for `exchange-manager.properties`. + +use std::collections::BTreeMap; + +use crate::controller::{TrinoRoleGroupConfig, ValidatedCluster}; + +/// Build the `exchange-manager.properties` key/value pairs. +/// +/// Returns an empty map when fault-tolerant execution is not configured and no user overrides are provided. +/// Callers should omit the file from the ConfigMap in that case. +pub fn build(cluster: &ValidatedCluster, rg: &TrinoRoleGroupConfig) -> BTreeMap { + let mut props = BTreeMap::new(); + + // 1. No defaults. + // 2. Automatic from resolved fault-tolerant-execution config. + if let Some(fte) = &cluster.cluster_config.fault_tolerant_execution { + props.extend(fte.exchange_manager_properties.clone()); + } + + // 3. No merged_config contribution. + // 4. User overrides (highest precedence). + props.extend(rg.config_overrides.exchange_manager_properties.clone()); + + props +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + controller::build::properties::test_support::{ + MINIMAL_TRINO_YAML, validated_cluster_from_yaml, + }, + crd::TrinoRole, + }; + + #[test] + fn default_renders_empty_when_no_fte() { + let cluster = validated_cluster_from_yaml(MINIMAL_TRINO_YAML); + let rg = cluster.role_group_configs[&TrinoRole::Coordinator] + .values() + .next() + .unwrap() + .clone(); + let props = build(&cluster, &rg); + assert!(props.is_empty()); + } +} diff --git a/rust/operator-binary/src/controller/build/properties/log_properties.rs b/rust/operator-binary/src/controller/build/properties/log_properties.rs new file mode 100644 index 000000000..6c5831b81 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/log_properties.rs @@ -0,0 +1,51 @@ +//! Builder for `log.properties`. + +use std::collections::BTreeMap; + +use crate::controller::TrinoRoleGroupConfig; + +/// Build the `log.properties` key/value pairs for `(role, rg)`. +/// +/// Returns `None`-equivalent (empty map) when there is nothing to write — +/// callers should omit the file from the ConfigMap if the result is empty. +pub fn build(rg: &TrinoRoleGroupConfig) -> BTreeMap { + let mut props = BTreeMap::new(); + + // 1. No defaults + // 2. Automatic per-container logger levels + if let Some(per_container) = + super::logging::get_log_property_map(&rg.config.logging.trino_container) + { + props.extend(per_container); + } + + // 3. No merged_config contribution. + // 4. User overrides (highest precedence). + props.extend(rg.config_overrides.log_properties.clone()); + + props +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + controller::build::properties::test_support::{ + MINIMAL_TRINO_YAML, validated_cluster_from_yaml, + }, + crd::TrinoRole, + }; + + #[test] + fn default_renders_root_logger_only() { + let cluster = validated_cluster_from_yaml(MINIMAL_TRINO_YAML); + let rg = cluster.role_group_configs[&TrinoRole::Coordinator] + .values() + .next() + .unwrap() + .clone(); + let props = build(&rg); + assert_eq!(props.get("").map(String::as_str), Some("info")); + assert!(!props.contains_key("io.trino")); + } +} diff --git a/rust/operator-binary/src/controller/build/properties/logging.rs b/rust/operator-binary/src/controller/build/properties/logging.rs new file mode 100644 index 000000000..f0067baee --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/logging.rs @@ -0,0 +1,58 @@ +use std::collections::BTreeMap; + +use stackable_operator::{ + product_logging::spec::{AutomaticContainerLogConfig, LogLevel}, + v2::product_logging::framework::ValidatedContainerLogConfigChoice, +}; +use strum::Display; + +#[derive(Display)] +#[strum(serialize_all = "lowercase")] +pub enum TrinoLogLevel { + Debug, + Info, + Warn, + Error, + Off, +} + +impl From for TrinoLogLevel { + fn from(level: LogLevel) -> Self { + match level { + LogLevel::TRACE | LogLevel::DEBUG => Self::Debug, + LogLevel::INFO => Self::Info, + LogLevel::WARN => Self::Warn, + LogLevel::ERROR | LogLevel::FATAL => Self::Error, + LogLevel::NONE => Self::Off, + } + } +} + +/// Return the `log.properties` content as a typed `BTreeMap` for the (validated) Trino container. +/// +/// Returns `None` when the Trino container uses a custom log ConfigMap (in which case the operator +/// does not generate `log.properties`). +pub fn get_log_property_map( + trino_container: &ValidatedContainerLogConfigChoice, +) -> Option> { + match trino_container { + ValidatedContainerLogConfigChoice::Automatic(log_config) => { + let map = log_config + .loggers + .iter() + .map(|(logger, config)| { + let log_level = TrinoLogLevel::from(config.level); + let key = if logger == AutomaticContainerLogConfig::ROOT_LOGGER { + // ROOT logger maps to an empty key in log.properties (=LEVEL). + String::new() + } else { + logger.clone() + }; + (key, log_level.to_string()) + }) + .collect(); + Some(map) + } + ValidatedContainerLogConfigChoice::Custom(_) => None, + } +} 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 000000000..76b2fb278 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/mod.rs @@ -0,0 +1,177 @@ +//! Per-file builders for Trino `.properties` files. +//! +//! Each `.rs` module produces the rendered key/value pairs for one +//! Trino config file. The shared config-file writer serializes the map to the +//! Java-properties on-wire format. + +pub mod access_control_properties; +pub mod config_properties; +pub mod exchange_manager_properties; +pub mod log_properties; +pub mod logging; +pub mod node_properties; +pub mod product_logging; +pub mod security_properties; +pub mod spooling_manager_properties; + +/// The names of the Trino `.properties` files assembled into the rolegroup ConfigMap. +#[derive(Clone, Copy, Debug, strum::Display)] +pub enum ConfigFileName { + #[strum(serialize = "config.properties")] + Config, + #[strum(serialize = "node.properties")] + Node, + #[strum(serialize = "log.properties")] + Log, + #[strum(serialize = "security.properties")] + Security, + #[strum(serialize = "access-control.properties")] + AccessControl, + #[strum(serialize = "exchange-manager.properties")] + ExchangeManager, + #[strum(serialize = "spooling-manager.properties")] + SpoolingManager, +} + +#[cfg(test)] +pub(crate) mod test_support { + use stackable_operator::cli::OperatorEnvironmentOptions; + + use crate::{ + controller::{ValidatedCluster, dereference::DereferencedObjects}, + crd::{authentication::ResolvedAuthenticationClassRef, v1alpha1}, + }; + + /// Dereferenced objects with no external resources resolved. + pub fn empty_derefs() -> DereferencedObjects { + DereferencedObjects { + resolved_authentication_classes: Vec::new(), + catalog_definitions: Vec::new(), + catalogs: Vec::new(), + trino_opa_config: None, + resolved_fte_config: None, + resolved_client_protocol_config: None, + } + } + + pub fn validated_cluster_from_yaml(yaml: &str) -> ValidatedCluster { + validated_cluster_from_yaml_with_derefs(yaml, empty_derefs()) + } + + /// Like [`validated_cluster_from_yaml`], but injects already-resolved AuthenticationClasses so + /// tests can exercise authentication-dependent branches. + pub fn validated_cluster_from_yaml_with_auth( + yaml: &str, + resolved_authentication_classes: Vec, + ) -> ValidatedCluster { + let mut derefs = empty_derefs(); + derefs.resolved_authentication_classes = resolved_authentication_classes; + validated_cluster_from_yaml_with_derefs(yaml, derefs) + } + + /// Validates `yaml` against the given (test-supplied) dereferenced objects, letting tests + /// exercise branches that depend on resolved inputs (e.g. fault-tolerant execution). + pub fn validated_cluster_from_yaml_with_derefs( + yaml: &str, + derefs: DereferencedObjects, + ) -> ValidatedCluster { + let trino: v1alpha1::TrinoCluster = serde_yaml::from_str(yaml).expect("invalid test YAML"); + let operator_env = OperatorEnvironmentOptions { + operator_namespace: "stackable-operators".to_string(), + operator_service_name: "trino-operator".to_string(), + image_repository: "oci.example.org".to_string(), + }; + crate::controller::validate::validate(&trino, &derefs, &operator_env) + .expect("validate should succeed for the test fixture") + } + + /// A resolved `static` (file) AuthenticationClass, enough to make + /// [`ValidatedClusterConfig::authentication_enabled`](crate::controller::ValidatedClusterConfig) + /// return `true`. + pub fn file_auth_class(name: &str) -> ResolvedAuthenticationClassRef { + let yaml = format!( + r#" + metadata: + name: {name} + spec: + provider: + static: + userCredentialsSecret: + name: {name} + "# + ); + let deserializer = serde_yaml::Deserializer::from_str(&yaml); + let authentication_class = + serde_yaml::with::singleton_map_recursive::deserialize(deserializer) + .expect("invalid test AuthenticationClass"); + ResolvedAuthenticationClassRef { + authentication_class, + client_auth_options: None, + } + } + + pub const MINIMAL_TRINO_YAML: &str = r#" + apiVersion: trino.stackable.tech/v1alpha1 + kind: TrinoCluster + metadata: + name: simple-trino + namespace: default + uid: "e6ac237d-a6d4-43a1-8135-f36506110912" + spec: + image: + productVersion: "479" + clusterConfig: + catalogLabelSelector: {} + coordinators: + roleGroups: + default: + replicas: 1 + workers: + roleGroups: + default: + replicas: 1 + "#; +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use stackable_operator::v2::config_file_writer::to_java_properties_string; + + fn render(pairs: &[(&str, &str)]) -> String { + let props: BTreeMap = pairs + .iter() + .map(|(k, v)| ((*k).to_string(), (*v).to_string())) + .collect(); + to_java_properties_string(props.iter()) + .expect("rendering the test properties should succeed") + } + + /// The escape behaviours pinned by the kuttl smoke snapshot + /// (`tests/templates/kuttl/smoke/14-assert.yaml.j2`). + #[test] + fn kuttl_pinned_escapes_are_stable() { + assert_eq!( + render(&[( + "internal-communication.shared-secret", + "${ENV:INTERNAL_SECRET}" + )]), + "internal-communication.shared-secret=${ENV\\:INTERNAL_SECRET}\n" + ); + assert_eq!( + render(&[("discovery.uri", "https://trino-coordinator.svc:8443")]), + "discovery.uri=https\\://trino-coordinator.svc\\:8443\n" + ); + } + + #[test] + fn keys_are_sorted_alphabetically() { + assert_eq!(render(&[("b", "2"), ("a", "1")]), "a=1\nb=2\n"); + } + + #[test] + fn empty_map_renders_empty_string() { + assert_eq!(render(&[]), ""); + } +} diff --git a/rust/operator-binary/src/controller/build/properties/node_properties.rs b/rust/operator-binary/src/controller/build/properties/node_properties.rs new file mode 100644 index 000000000..2046b56d5 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/node_properties.rs @@ -0,0 +1,50 @@ +//! Builder for `node.properties`. + +use std::collections::BTreeMap; + +use crate::controller::{TrinoRoleGroupConfig, ValidatedCluster}; + +const NODE_ENVIRONMENT: &str = "node.environment"; + +/// Build the `node.properties` key/value pairs. +/// +/// `node.environment` is derived from the cluster name: lowercased, with `-` +/// replaced by `_`. Trino requires `^[a-z][a-z0-9_]*[a-z0-9]$`; cluster names +/// constrained by Kubernetes naming already satisfy this after the transform. +pub fn build(cluster: &ValidatedCluster, rg: &TrinoRoleGroupConfig) -> BTreeMap { + let mut props = BTreeMap::new(); + + // 1. No defaults. + // 2. Automatic derived from cluster name. + let node_env = cluster.name.as_ref().to_ascii_lowercase().replace('-', "_"); + props.insert(NODE_ENVIRONMENT.to_string(), node_env); + + // 3. No merged_config contribution for node.properties. + // 4. User overrides (highest precedence). + props.extend(rg.config_overrides.node_properties.clone()); + + props +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::controller::build::properties::test_support::{ + MINIMAL_TRINO_YAML, validated_cluster_from_yaml, + }; + + #[test] + fn default_renders_node_environment_from_cluster_name() { + let cluster = validated_cluster_from_yaml(MINIMAL_TRINO_YAML); + let rg = cluster.role_group_configs[&crate::crd::TrinoRole::Coordinator] + .values() + .next() + .unwrap() + .clone(); + let props = build(&cluster, &rg); + assert_eq!( + props.get("node.environment").map(String::as_str), + Some("simple_trino"), + ); + } +} 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 000000000..59950da6d --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/product_logging/mod.rs @@ -0,0 +1,28 @@ +//! The Vector agent configuration (`vector.yaml`) assembled into the rolegroup `ConfigMap`. + +/// The Vector agent configuration (`vector.yaml`). +/// +/// It is templated with environment variables (`${LOG_DIR}`, `${NAMESPACE}`, …) that the +/// Vector container injects at runtime, so the same file content is used for every +/// rolegroup. +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() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_vector_config_file_content() { + let content = vector_config_file_content(); + assert!(!content.is_empty()); + // The airlift source must be present (Trino's main log format) ... + assert!(content.contains("files_airlift")); + // ... while the aggregator sink must reference the injected address. + assert!(content.contains("${VECTOR_AGGREGATOR_ADDRESS}")); + } +} 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 000000000..a5cb43149 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/product_logging/test-vector.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env sh + +# Trino emits airlift timestamps as UTC (trailing `Z`). The shared transform matches the `Z` +# literally, so `parse_timestamp` assumes the host timezone; pin it to UTC (as the pods run) to +# keep the tests deterministic regardless of the host's timezone. +TZ=UTC \ +DATA_DIR=/stackable/log/_vector-state \ +LOG_DIR=/stackable/log \ +NAMESPACE=default \ +CLUSTER_NAME=trino \ +ROLE_NAME=coordinator \ +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 000000000..f6af98be2 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/product_logging/vector-test.yaml @@ -0,0 +1,159 @@ +# 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 stdout log entry + inputs: + - type: log + insert_at: processed_files_stdout + log_fields: + file: /stackable/log/trino/trino.stdout.log + message: Starting Trino + pod: trino-coordinator-default-0 + source_type: file + timestamp: 2025-10-02T09:27:28.582Z + outputs: + - extract_from: extended_logs + conditions: + - type: vrl + source: | + expected_log_event = { + "cluster": "trino", + "container": "trino", + "file": "trino.stdout.log", + "level": "INFO", + "logger": "ROOT", + "message": "Starting Trino", + "namespace": "default", + "pod": "trino-coordinator-default-0", + "role": "coordinator", + "roleGroup": "default", + "timestamp": "2025-10-02T09:27:28.582Z" + } + + assert_eq!(expected_log_event, .) + - name: Test stderr log entry + inputs: + - type: log + insert_at: processed_files_stderr + log_fields: + file: /stackable/log/trino/trino.stderr.log + message: "Exception in thread \"main\"" + pod: trino-coordinator-default-0 + source_type: file + timestamp: 2025-10-02T09:27:28.582Z + outputs: + - extract_from: extended_logs + conditions: + - type: vrl + source: | + expected_log_event = { + "cluster": "trino", + "container": "trino", + "file": "trino.stderr.log", + "level": "ERROR", + "logger": "ROOT", + "message": "Exception in thread \"main\"", + "namespace": "default", + "pod": "trino-coordinator-default-0", + "role": "coordinator", + "roleGroup": "default", + "timestamp": "2025-10-02T09:27:28.582Z" + } + + assert_eq!(expected_log_event, .) + - name: Test airlift JSON log entry + inputs: + - type: log + insert_at: processed_files_airlift + log_fields: + file: /stackable/log/trino/server.airlift.json + message: '{"timestamp":"2024-01-01T00:00:00.123456789Z","level":"INFO","logger":"io.trino.server.Server","thread":"main","message":"======== SERVER STARTED ========"}' + pod: trino-coordinator-default-0 + source_type: file + outputs: + - extract_from: extended_logs + conditions: + - type: vrl + source: | + expected_log_event = { + "cluster": "trino", + "container": "trino", + "file": "server.airlift.json", + "level": "INFO", + "logger": "io.trino.server.Server", + "message": "======== SERVER STARTED ========", + "namespace": "default", + "pod": "trino-coordinator-default-0", + "role": "coordinator", + "roleGroup": "default", + "thread": "main", + # `timestamp` is the airlift `timestamp` field parsed to a real timestamp value + # (nanosecond precision preserved). Asserted as a timestamp literal, not a string. + "timestamp": t'2024-01-01T00:00:00.123456789Z' + } + + 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: trino-coordinator-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": "trino", + "container": "vector", + "level": "INFO", + "logger": "vector::internal_events::process", + "message": "Vector has started.", + "namespace": "default", + "pod": "trino-coordinator-default-0", + "role": "coordinator", + "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 000000000..0d922ac58 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/product_logging/vector.yaml @@ -0,0 +1,163 @@ +--- +data_dir: ${DATA_DIR} + +log_schema: + host_key: pod + +sources: + # Reads the internal Vector logs + vector: + type: internal_logs + + files_stdout: + type: file + include: + - ${LOG_DIR}/*/*.stdout.log + + files_stderr: + type: file + include: + - ${LOG_DIR}/*/*.stderr.log + + # Trino logs in the airlift JSON format (see `log.path` / `server.airlift.json`). + files_airlift: + type: file + include: + - ${LOG_DIR}/*/*.airlift.json + +transforms: + 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_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") + } + + # Extends the processed files with the fields "container" and "file" + extended_logs_files: + inputs: + - processed_files_* + type: remap + source: | + del(.source_type) + if .errors == [] { + del(.errors) + } + . |= parse_regex!(.file, r'^${LOG_DIR}/(?P.*?)/(?P.*?)$') + + # Filters the logs of the Vector agent according to the defined log level + filtered_logs_vector: + inputs: + - vector + type: filter + condition: > + (.metadata.level == "TRACE" && "${VECTOR_FILE_LOG_LEVEL}" == "trace") || + (.metadata.level == "DEBUG" && includes(["trace", "debug"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "INFO" && includes(["trace", "debug", "info"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "WARN" && includes(["trace", "debug", "info", "warn"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "ERROR" && includes(["trace", "debug", "info", "warn", "error"], "${VECTOR_FILE_LOG_LEVEL}")) + + # Aligns the logs of the Vector agent with the common format + 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) + + # Add the fields "namespace", "cluster", "role" and "roleGroup" to all logs + extended_logs: + inputs: + - extended_logs_* + type: remap + source: | + .namespace = "${NAMESPACE}" + .cluster = "${CLUSTER_NAME}" + .role = "${ROLE_NAME}" + .roleGroup = "${ROLE_GROUP_NAME}" + +sinks: + # Forward the logs to the Vector aggregator + 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 000000000..70a932d89 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/security_properties.rs @@ -0,0 +1,122 @@ +//! Builder for `security.properties` (Trino's JVM security properties file). + +use std::collections::BTreeMap; + +use crate::controller::TrinoRoleGroupConfig; + +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. +/// +/// Both keys apply to both `coordinator` and `worker` roles. +pub fn build(rg: &TrinoRoleGroupConfig) -> BTreeMap { + let mut props = BTreeMap::new(); + + // 1. Defaults + 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. No automatic operator-injected values. + // 3. No merged_config contribution. + // 4. User overrides (highest precedence). + props.extend(rg.config_overrides.security_properties.clone()); + + props +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + controller::build::properties::test_support::{ + MINIMAL_TRINO_YAML, validated_cluster_from_yaml, + }, + crd::TrinoRole, + }; + + fn coordinator_rg( + cluster: &crate::controller::ValidatedCluster, + ) -> crate::controller::TrinoRoleGroupConfig { + cluster.role_group_configs[&TrinoRole::Coordinator] + .values() + .next() + .unwrap() + .clone() + } + + #[test] + fn default_renders_networkaddress_cache_settings() { + let cluster = validated_cluster_from_yaml(MINIMAL_TRINO_YAML); + let props = build(&coordinator_rg(&cluster)); + assert_eq!( + props.get("networkaddress.cache.ttl").map(String::as_str), + Some("30") + ); + assert_eq!( + props + .get("networkaddress.cache.negative.ttl") + .map(String::as_str), + Some("0") + ); + } + + #[test] + fn user_override_wins_and_extra_key_is_added() { + let cluster = validated_cluster_from_yaml( + r#" + apiVersion: trino.stackable.tech/v1alpha1 + kind: TrinoCluster + metadata: + name: simple-trino + namespace: default + uid: "e6ac237d-a6d4-43a1-8135-f36506110912" + spec: + image: + productVersion: "479" + clusterConfig: + catalogLabelSelector: {} + coordinators: + roleGroups: + default: + replicas: 1 + configOverrides: + security.properties: + networkaddress.cache.ttl: "99" + custom.extra.key: "myvalue" + workers: + roleGroups: + default: + replicas: 1 + "#, + ); + let props = build(&coordinator_rg(&cluster)); + + // User override wins over the default. + assert_eq!( + props.get("networkaddress.cache.ttl").map(String::as_str), + Some("99") + ); + // Extra (non-default) override key is added. + assert_eq!( + props.get("custom.extra.key").map(String::as_str), + Some("myvalue") + ); + // Untouched default remains. + assert_eq!( + props + .get("networkaddress.cache.negative.ttl") + .map(String::as_str), + Some("0") + ); + } +} diff --git a/rust/operator-binary/src/controller/build/properties/spooling_manager_properties.rs b/rust/operator-binary/src/controller/build/properties/spooling_manager_properties.rs new file mode 100644 index 000000000..94a6d63a9 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/spooling_manager_properties.rs @@ -0,0 +1,48 @@ +//! Builder for `spooling-manager.properties`. + +use std::collections::BTreeMap; + +use crate::controller::{TrinoRoleGroupConfig, ValidatedCluster}; + +/// Build the `spooling-manager.properties` key/value pairs. +/// +/// Returns an empty map when client spooling is not configured and no user overrides are provided. +/// Callers should omit the file from the ConfigMap in that case. +pub fn build(cluster: &ValidatedCluster, rg: &TrinoRoleGroupConfig) -> BTreeMap { + let mut props = BTreeMap::new(); + + // 1. No defaults. + // 2. Automatic from resolved client-spooling protocol config. + if let Some(spooling) = &cluster.cluster_config.client_protocol { + props.extend(spooling.spooling_manager_properties.clone()); + } + + // 3. No merged_config contribution. + // 4. User overrides (highest precedence). + props.extend(rg.config_overrides.spooling_manager_properties.clone()); + + props +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + controller::build::properties::test_support::{ + MINIMAL_TRINO_YAML, validated_cluster_from_yaml, + }, + crd::TrinoRole, + }; + + #[test] + fn default_renders_empty_when_no_spooling() { + let cluster = validated_cluster_from_yaml(MINIMAL_TRINO_YAML); + let rg = cluster.role_group_configs[&TrinoRole::Coordinator] + .values() + .next() + .unwrap() + .clone(); + let props = build(&cluster, &rg); + assert!(props.is_empty()); + } +} 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 000000000..37bb891e7 --- /dev/null +++ b/rust/operator-binary/src/controller/build/resource/config_map.rs @@ -0,0 +1,228 @@ +//! Build per-rolegroup `ConfigMap` for the Trino cluster. + +use std::collections::BTreeMap; + +use snafu::{OptionExt, ResultExt, Snafu}; +use stackable_operator::{ + builder::configmap::ConfigMapBuilder, k8s_openapi::api::core::v1::ConfigMap, kvp::Labels, + product_logging::framework::VECTOR_CONFIG_FILE, utils::cluster_info::KubernetesClusterInfo, + v2::config_file_writer::to_java_properties_string, +}; + +use crate::{ + config::jvm, + controller::{ + RoleGroupName, ValidatedCluster, + build::properties::{ + ConfigFileName, access_control_properties, config_properties, + exchange_manager_properties, log_properties, node_properties, product_logging, + security_properties, spooling_manager_properties, + }, + }, + crd::TrinoRole, +}; + +// File name not exported from crd/mod.rs. +const JVM_CONFIG: &str = "jvm.config"; + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("failed to build config.properties"))] + BuildConfigProperties { source: config_properties::Error }, + + #[snafu(display("failed to write {file} properties"))] + WriteProperties { + source: stackable_operator::v2::config_file_writer::PropertiesWriterError, + file: String, + }, + + #[snafu(display("missing rolegroup {role_group} under role {role}"))] + MissingRoleGroup { role: String, role_group: String }, + + #[snafu(display("failed to assemble ConfigMap for {rolegroup}"))] + Assemble { + source: stackable_operator::builder::configmap::Error, + rolegroup: String, + }, + + #[snafu(display("failed to build jvm.config"))] + BuildJvmConfig { source: crate::config::jvm::Error }, +} + +type Result = std::result::Result; + +pub fn build_rolegroup_config_map( + cluster: &ValidatedCluster, + role: &TrinoRole, + role_group_name: &RoleGroupName, + cluster_info: &KubernetesClusterInfo, + recommended_labels: &Labels, +) -> Result { + let role_group_configs = + cluster + .role_group_configs + .get(role) + .with_context(|| MissingRoleGroupSnafu { + role: role.to_string(), + role_group: role_group_name.to_string(), + })?; + let rg = role_group_configs + .get(role_group_name) + .with_context(|| MissingRoleGroupSnafu { + role: role.to_string(), + role_group: role_group_name.to_string(), + })?; + + let config_map_name = cluster + .resource_names(role, role_group_name) + .role_group_config_map() + .to_string(); + + let mut data: BTreeMap = BTreeMap::new(); + + // Auth files (e.g. password-authenticator file contents). + for (file_name, props) in cluster.cluster_config.authentication.config_files(role) { + let rendered = + to_java_properties_string(props.iter()).with_context(|_| WritePropertiesSnafu { + file: file_name.clone(), + })?; + data.insert(file_name, rendered); + } + + // 1. config.properties (fallible). + let cfg = config_properties::build(cluster, role.clone(), rg, cluster_info) + .context(BuildConfigPropertiesSnafu)?; + data.insert( + ConfigFileName::Config.to_string(), + to_java_properties_string(cfg.iter()).with_context(|_| WritePropertiesSnafu { + file: ConfigFileName::Config.to_string(), + })?, + ); + + // 2. node.properties. + let node = node_properties::build(cluster, rg); + data.insert( + ConfigFileName::Node.to_string(), + to_java_properties_string(node.iter()).with_context(|_| WritePropertiesSnafu { + file: ConfigFileName::Node.to_string(), + })?, + ); + + // 3. log.properties (optional — empty map → omit). + let log = log_properties::build(rg); + if !log.is_empty() { + data.insert( + ConfigFileName::Log.to_string(), + to_java_properties_string(log.iter()).with_context(|_| WritePropertiesSnafu { + file: ConfigFileName::Log.to_string(), + })?, + ); + } + + // 4. security.properties. + let sec = security_properties::build(rg); + data.insert( + ConfigFileName::Security.to_string(), + to_java_properties_string(sec.iter()).with_context(|_| WritePropertiesSnafu { + file: ConfigFileName::Security.to_string(), + })?, + ); + + // 5. access-control.properties (optional). + let ac = access_control_properties::build(cluster, rg); + if !ac.is_empty() { + data.insert( + ConfigFileName::AccessControl.to_string(), + to_java_properties_string(ac.iter()).with_context(|_| WritePropertiesSnafu { + file: ConfigFileName::AccessControl.to_string(), + })?, + ); + } + + // 6. exchange-manager.properties (optional). + let em = exchange_manager_properties::build(cluster, rg); + if !em.is_empty() { + data.insert( + ConfigFileName::ExchangeManager.to_string(), + to_java_properties_string(em.iter()).with_context(|_| WritePropertiesSnafu { + file: ConfigFileName::ExchangeManager.to_string(), + })?, + ); + } + + // 7. spooling-manager.properties (optional). + let sm = spooling_manager_properties::build(cluster, rg); + if !sm.is_empty() { + data.insert( + ConfigFileName::SpoolingManager.to_string(), + to_java_properties_string(sm.iter()).with_context(|_| WritePropertiesSnafu { + file: ConfigFileName::SpoolingManager.to_string(), + })?, + ); + } + + // 8. jvm.config. The role + role-group `jvmArgumentOverrides` were already merged in the + // validate step and are carried by `product_specific_common_config`. + let jvm_config = jvm::jvm_config( + cluster.product_version, + &rg.config, + &rg.product_specific_common_config.jvm_argument_overrides, + ) + .context(BuildJvmConfigSnafu)?; + data.insert(JVM_CONFIG.to_string(), jvm_config); + + // 9. Vector agent config (`vector.yaml`) if the Vector agent is enabled. The file is templated + // with environment variables injected by the Vector container at runtime. + if rg.config.logging.enable_vector_agent { + data.insert( + VECTOR_CONFIG_FILE.to_string(), + product_logging::vector_config_file_content(), + ); + } + + ConfigMapBuilder::new() + .metadata( + cluster + .object_meta(&config_map_name, recommended_labels.clone()) + .build(), + ) + .data(data) + .build() + .with_context(|_| AssembleSnafu { + rolegroup: config_map_name.clone(), + }) +} + +/// The rolegroup catalog [`ConfigMap`] configures the rolegroup catalog based on the configuration +/// given by the administrator +pub fn build_rolegroup_catalog_config_map( + cluster: &ValidatedCluster, + role: &TrinoRole, + role_group_name: &RoleGroupName, + recommended_labels: &Labels, +) -> Result { + let catalog_config_map_name = cluster.role_group_catalog_config_map_name(role, role_group_name); + ConfigMapBuilder::new() + .metadata( + cluster + .object_meta(&catalog_config_map_name, recommended_labels.clone()) + .build(), + ) + .data( + cluster + .cluster_config + .catalogs + .iter() + .map(|catalog| { + let file = format!("{}.properties", catalog.name); + let rendered = to_java_properties_string(catalog.properties.iter()) + .with_context(|_| WritePropertiesSnafu { file: file.clone() })?; + Ok((file, rendered)) + }) + .collect::>()?, + ) + .build() + .with_context(|_| AssembleSnafu { + rolegroup: catalog_config_map_name.clone(), + }) +} diff --git a/rust/operator-binary/src/listener.rs b/rust/operator-binary/src/controller/build/resource/listener.rs similarity index 54% rename from rust/operator-binary/src/listener.rs rename to rust/operator-binary/src/controller/build/resource/listener.rs index ef12ef0d3..68458ccc1 100644 --- a/rust/operator-binary/src/listener.rs +++ b/rust/operator-binary/src/controller/build/resource/listener.rs @@ -1,32 +1,24 @@ +use std::str::FromStr; + use snafu::{ResultExt, Snafu}; use stackable_operator::{ - builder::{ - meta::ObjectMetaBuilder, - pod::volume::{ListenerOperatorVolumeSourceBuilder, ListenerReference}, - }, + builder::pod::volume::{ListenerOperatorVolumeSourceBuilder, ListenerReference}, crd::listener::v1alpha1::{Listener, ListenerPort, ListenerSpec}, k8s_openapi::api::core::v1::PersistentVolumeClaim, - kube::ResourceExt, - kvp::{Labels, ObjectLabels}, + kvp::Labels, + v2::types::kubernetes::{ListenerClassName, VolumeName}, }; -use crate::crd::{TrinoRole, v1alpha1}; +use crate::{ + controller::{ValidatedCluster, build::ports}, + crd::TrinoRole, +}; -pub const LISTENER_VOLUME_NAME: &str = "listener"; +stackable_operator::constant!(pub LISTENER_VOLUME_NAME: VolumeName = "listener"); pub const LISTENER_VOLUME_DIR: &str = "/stackable/listener"; #[derive(Snafu, Debug)] pub enum Error { - #[snafu(display("listener object is missing metadata to build owner reference"))] - ObjectMissingMetadataForOwnerRef { - source: stackable_operator::builder::meta::Error, - }, - - #[snafu(display("failed to build listener object meta data"))] - BuildObjectMeta { - source: stackable_operator::builder::meta::Error, - }, - #[snafu(display("failed to build listener volume"))] BuildListenerPersistentVolume { source: stackable_operator::builder::pod::volume::ListenerOperatorVolumeSourceBuilderError, @@ -34,27 +26,22 @@ pub enum Error { } pub fn build_group_listener( - trino: &v1alpha1::TrinoCluster, - object_labels: ObjectLabels, - listener_class: String, + cluster: &ValidatedCluster, + recommended_labels: Labels, + listener_class: &ListenerClassName, listener_group_name: String, -) -> Result { - Ok(Listener { - metadata: ObjectMetaBuilder::new() - .name_and_namespace(trino) - .name(listener_group_name) - .ownerreference_from_resource(trino, None, Some(true)) - .context(ObjectMissingMetadataForOwnerRefSnafu)? - .with_recommended_labels(&object_labels) - .context(BuildObjectMetaSnafu)? +) -> Listener { + Listener { + metadata: cluster + .object_meta(listener_group_name, recommended_labels) .build(), spec: ListenerSpec { - class_name: Some(listener_class), - ports: Some(listener_ports(trino)), + class_name: Some(listener_class.to_string()), + ports: Some(listener_ports(cluster)), ..ListenerSpec::default() }, status: None, - }) + } } pub fn build_group_listener_pvc( @@ -72,11 +59,11 @@ pub fn build_group_listener_pvc( /// The name of the group-listener provided for a specific role-group. /// Coordinator(s) will use this group listener so that only one load balancer /// is needed (per role group). -pub fn group_listener_name(trino: &v1alpha1::TrinoCluster, role: &TrinoRole) -> Option { +pub fn group_listener_name(cluster: &ValidatedCluster, role: &TrinoRole) -> Option { match role { TrinoRole::Coordinator => Some(format!( "{cluster_name}-{role}", - cluster_name = trino.name_any() + cluster_name = cluster.name )), TrinoRole::Worker => None, } @@ -91,9 +78,9 @@ pub fn secret_volume_listener_scope(role: &TrinoRole) -> Option { } /// We only use the http/https port here and intentionally omit the metrics one. -fn listener_ports(trino: &v1alpha1::TrinoCluster) -> Vec { - let name = trino.exposed_protocol().to_string(); - let port = trino.exposed_port().into(); +fn listener_ports(cluster: &ValidatedCluster) -> Vec { + let name = ports::exposed_protocol(cluster).to_string(); + let port = ports::exposed_port(cluster).into(); vec![ListenerPort { name, 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 000000000..0c96e5909 --- /dev/null +++ b/rust/operator-binary/src/controller/build/resource/mod.rs @@ -0,0 +1,11 @@ +//! Builders for the individual Kubernetes resources of a TrinoCluster. +//! +//! Each submodule builds one kind of resource (ConfigMap, Service, Listener, PDB, …) and +//! *returns* it, rather than mutating shared state or applying it directly. The reconciler +//! collects the returned objects and applies them. + +pub mod config_map; +pub mod listener; +pub mod pdb; +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 000000000..9f81f2cc0 --- /dev/null +++ b/rust/operator-binary/src/controller/build/resource/pdb.rs @@ -0,0 +1,104 @@ +use std::{cmp::max, str::FromStr}; + +use stackable_operator::{ + commons::pdb::PdbConfig, + k8s_openapi::api::policy::v1::PodDisruptionBudget, + v2::{builder::pdb::pod_disruption_budget_builder_with_role, types::operator::RoleName}, +}; + +use crate::{ + controller::{ValidatedCluster, controller_name, operator_name, product_name}, + crd::TrinoRole, +}; + +/// Builds the [`PodDisruptionBudget`] for the given `role`, or `None` if PDBs are disabled. +/// +/// The reconciler applies the returned object; this function does not touch the cluster. +pub fn build_pdb( + pdb: &PdbConfig, + cluster: &ValidatedCluster, + role: &TrinoRole, +) -> Option { + if !pdb.enabled { + return None; + } + let max_unavailable = pdb.max_unavailable.unwrap_or(match role { + TrinoRole::Coordinator => max_unavailable_coordinators(), + TrinoRole::Worker => max_unavailable_workers(worker_count(cluster)), + }); + let role_name = + RoleName::from_str(&role.to_string()).expect("a TrinoRole is a valid RFC 1123 role name"); + let pdb = pod_disruption_budget_builder_with_role( + cluster, + &product_name(), + &role_name, + &operator_name(), + &controller_name(), + ) + .with_max_unavailable(max_unavailable) + .build(); + + Some(pdb) +} + +/// Total number of worker replicas across all worker role groups. +/// +/// Role groups without an explicit replica count (i.e. those left to a HorizontalPodAutoscaler) +/// contribute nothing, as their size is not known at reconcile time. +fn worker_count(cluster: &ValidatedCluster) -> u16 { + cluster + .role_group_configs + .get(&TrinoRole::Worker) + .into_iter() + .flat_map(|groups| groups.values()) + .filter_map(|rg| rg.replicas) + .sum() +} + +fn max_unavailable_coordinators() -> u16 { + 1 +} + +fn max_unavailable_workers(num_workers: u16) -> u16 { + // As users normally scale Trino workers to achieve more performance, we can safely take out 10% of the workers. + let max_unavailable = num_workers / 10; + + // Clamp to at least a single node allowed to be offline, so we don't block Kubernetes nodes from draining. + max(max_unavailable, 1) +} + +#[cfg(test)] +mod test { + use rstest::rstest; + + use super::*; + + #[rstest] + #[case(0, 1)] + #[case(1, 1)] + #[case(2, 1)] + #[case(3, 1)] + #[case(4, 1)] + #[case(5, 1)] + #[case(6, 1)] + #[case(7, 1)] + #[case(8, 1)] + #[case(9, 1)] + #[case(10, 1)] + #[case(11, 1)] + #[case(12, 1)] + #[case(19, 1)] + #[case(20, 2)] + #[case(21, 2)] + #[case(29, 2)] + #[case(30, 3)] + #[case(31, 3)] + #[case(100, 10)] + fn test_max_unavailable_servers( + #[case] num_workers: u16, + #[case] expected_max_unavailable: u16, + ) { + let max_unavailable = max_unavailable_workers(num_workers); + assert_eq!(max_unavailable, expected_max_unavailable); + } +} diff --git a/rust/operator-binary/src/service.rs b/rust/operator-binary/src/controller/build/resource/service.rs similarity index 56% rename from rust/operator-binary/src/service.rs rename to rust/operator-binary/src/controller/build/resource/service.rs index 1ee44bad2..2974bec88 100644 --- a/rust/operator-binary/src/service.rs +++ b/rust/operator-binary/src/controller/build/resource/service.rs @@ -1,78 +1,65 @@ use std::collections::BTreeMap; -use snafu::{ResultExt, Snafu}; use stackable_operator::{ - builder::meta::ObjectMetaBuilder, k8s_openapi::api::core::v1::{Service, ServicePort, ServiceSpec}, - kvp::{Annotations, Labels, ObjectLabels}, - role_utils::RoleGroupRef, + kvp::{Annotations, Labels}, }; -use crate::crd::{METRICS_PORT, METRICS_PORT_NAME, v1alpha1}; - -#[derive(Snafu, Debug)] -pub enum Error { - #[snafu(display("object is missing metadata to build owner reference"))] - ObjectMissingMetadataForOwnerRef { - source: stackable_operator::builder::meta::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, - }, -} +use crate::{ + controller::{RoleGroupName, ValidatedCluster, build::ports}, + crd::{METRICS_PORT, METRICS_PORT_NAME, TrinoRole}, +}; /// The rolegroup headless [`Service`] is a 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( - trino: &v1alpha1::TrinoCluster, - role_group_ref: &RoleGroupRef, - object_labels: ObjectLabels, + cluster: &ValidatedCluster, + role: &TrinoRole, + role_group_name: &RoleGroupName, + recommended_labels: &Labels, selector: BTreeMap, -) -> Result { - Ok(Service { - metadata: ObjectMetaBuilder::new() - .name_and_namespace(trino) - .name(role_group_ref.rolegroup_headless_service_name()) - .ownerreference_from_resource(trino, None, Some(true)) - .context(ObjectMissingMetadataForOwnerRefSnafu)? - .with_recommended_labels(&object_labels) - .context(MetadataBuildSnafu)? + ports: Vec, +) -> Service { + Service { + metadata: cluster + .object_meta( + cluster + .resource_names(role, role_group_name) + .headless_service_name() + .to_string(), + recommended_labels.clone(), + ) .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(headless_service_ports(trino)), + ports: Some(ports), selector: Some(selector), 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( - trino: &v1alpha1::TrinoCluster, - role_group_ref: &RoleGroupRef, - object_labels: ObjectLabels, + cluster: &ValidatedCluster, + role: &TrinoRole, + role_group_name: &RoleGroupName, + recommended_labels: &Labels, selector: BTreeMap, -) -> Result { - Ok(Service { - metadata: ObjectMetaBuilder::new() - .name_and_namespace(trino) - .name(role_group_ref.rolegroup_metrics_service_name()) - .ownerreference_from_resource(trino, None, Some(true)) - .context(ObjectMissingMetadataForOwnerRefSnafu)? - .with_recommended_labels(&object_labels) - .context(MetadataBuildSnafu)? +) -> Service { + Service { + metadata: cluster + .object_meta( + cluster + .resource_names(role, role_group_name) + .metrics_service_name() + .to_string(), + recommended_labels.clone(), + ) .with_labels(prometheus_labels()) .with_annotations(prometheus_annotations()) .build(), @@ -86,12 +73,12 @@ pub fn build_rolegroup_metrics_service( ..ServiceSpec::default() }), status: None, - }) + } } -fn headless_service_ports(trino: &v1alpha1::TrinoCluster) -> Vec { - let name = trino.exposed_protocol().to_string(); - let port = trino.exposed_port().into(); +pub(crate) fn headless_service_ports(cluster: &ValidatedCluster) -> Vec { + let name = ports::exposed_protocol(cluster).to_string(); + let port = ports::exposed_port(cluster).into(); vec![ServicePort { name: Some(name), 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 000000000..19ed2321c --- /dev/null +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -0,0 +1,786 @@ +//! Builds the per-rolegroup [`StatefulSet`] that runs a Trino role group. + +use std::{convert::Infallible, str::FromStr}; + +use snafu::{OptionExt, ResultExt, Snafu}; +use stackable_operator::{ + builder::{ + meta::ObjectMetaBuilder, + pod::{ + PodBuilder, + container::ContainerBuilder, + resources::ResourceRequirementsBuilder, + security::PodSecurityContextBuilder, + volume::{SecretFormat, SecretOperatorVolumeSourceBuilder, VolumeBuilder}, + }, + }, + commons::secret_class::SecretClassVolumeProvisionParts, + constants::RESTART_CONTROLLER_ENABLED_LABEL, + k8s_openapi::{ + DeepMerge, + api::{ + apps::v1::{StatefulSet, StatefulSetSpec}, + core::v1::{ + ConfigMapVolumeSource, ContainerPort, EnvVar, EnvVarSource, ExecAction, + HTTPGetAction, Probe, SecretKeySelector, Volume, + }, + }, + apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString}, + }, + kvp::Annotation, + product_logging, + shared::time::Duration, + v2::{ + builder::{pod::container::EnvVarSet, statefulset::restarter_ignore_secret_annotations}, + product_logging::framework::{ValidatedContainerLogConfigChoice, vector_container}, + types::kubernetes::{ContainerName, SecretClassName, VolumeName}, + }, +}; + +use crate::{ + authorization::opa::OPA_TLS_VOLUME_NAME, + controller::{ + RoleGroupName, TrinoRoleGroupConfig, ValidatedCluster, build, + build::{ + command, + resource::listener::{ + LISTENER_VOLUME_DIR, LISTENER_VOLUME_NAME, build_group_listener_pvc, + group_listener_name, secret_volume_listener_scope, + }, + }, + }, + crd::{ + CONFIG_DIR_NAME, Container, ENV_INTERNAL_SECRET, ENV_SPOOLING_SECRET, HTTP_PORT, + HTTP_PORT_NAME, HTTPS_PORT, HTTPS_PORT_NAME, MAX_TRINO_LOG_FILES_SIZE, METRICS_PORT, + METRICS_PORT_NAME, RW_CONFIG_DIR_NAME, STACKABLE_CLIENT_TLS_DIR, + STACKABLE_INTERNAL_TLS_DIR, STACKABLE_MOUNT_INTERNAL_TLS_DIR, + STACKABLE_MOUNT_SERVER_TLS_DIR, STACKABLE_SERVER_TLS_DIR, STACKABLE_TLS_STORE_PASSWORD, + TrinoRole, + }, + trino_controller::{ + MAX_PREPARE_LOG_FILE_SIZE, STACKABLE_LOG_CONFIG_DIR, STACKABLE_LOG_DIR, + shared_internal_secret_name, shared_spooling_secret_name, + }, +}; + +stackable_operator::constant!(VECTOR_CONTAINER_NAME: ContainerName = "vector"); + +// Typed names for the Pod's volumes, so each volume definition and its matching volume mounts +// stay in sync. The Vector agent reads its `vector.yaml` from the rolegroup ConfigMap (mounted as +// the `config` volume) and writes its state under the shared `log` volume. +stackable_operator::constant!(CONFIG_VOLUME_NAME: VolumeName = "config"); +stackable_operator::constant!(RW_CONFIG_VOLUME_NAME: VolumeName = "rwconfig"); +stackable_operator::constant!(CATALOG_VOLUME_NAME: VolumeName = "catalog"); +stackable_operator::constant!(LOG_CONFIG_VOLUME_NAME: VolumeName = "log-config"); +// `log` is also mounted by the password-file-updater container built in the authentication module. +stackable_operator::constant!(pub LOG_VOLUME_NAME: VolumeName = "log"); +stackable_operator::constant!(SERVER_TLS_MOUNT_VOLUME_NAME: VolumeName = "server-tls-mount"); +stackable_operator::constant!(SERVER_TLS_VOLUME_NAME: VolumeName = "server-tls"); +stackable_operator::constant!(CLIENT_TLS_VOLUME_NAME: VolumeName = "client-tls"); +stackable_operator::constant!(INTERNAL_TLS_MOUNT_VOLUME_NAME: VolumeName = "internal-tls-mount"); +stackable_operator::constant!(INTERNAL_TLS_VOLUME_NAME: VolumeName = "internal-tls"); + +#[derive(Snafu, Debug)] +pub enum Error { + #[snafu(display("missing secret lifetime"))] + MissingSecretLifetime, + + #[snafu(display("illegal container name: [{container_name}]"))] + IllegalContainerName { + source: stackable_operator::builder::pod::container::Error, + container_name: String, + }, + + #[snafu(display("failed to configure graceful shutdown"))] + GracefulShutdown { + source: build::graceful_shutdown::Error, + }, + + #[snafu(display("failed to build Annotation"))] + AnnotationBuild { + source: stackable_operator::kvp::KeyValuePairError, + }, + + #[snafu(display("failed to build TLS certificate SecretClass Volume"))] + TlsCertSecretClassVolumeBuild { + source: stackable_operator::builder::pod::volume::SecretOperatorVolumeSourceBuilderError, + }, + + #[snafu(display("failed to add needed volume"))] + AddVolume { + source: stackable_operator::builder::pod::Error, + }, + + #[snafu(display("failed to add needed volumeMount"))] + AddVolumeMount { + source: stackable_operator::builder::pod::container::Error, + }, + + #[snafu(display("invalid Trino authentication"))] + InvalidAuthenticationConfig { + source: crate::authentication::Error, + }, + + #[snafu(display("failed to configure listener"))] + ListenerConfiguration { + source: build::resource::listener::Error, + }, +} + +type Result = std::result::Result; + +/// The 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 [`stackable_operator::k8s_openapi::api::core::v1::Service`] (from +/// [`build_rolegroup_headless_service`](super::service::build_rolegroup_headless_service)). +pub fn build_rolegroup_statefulset( + cluster: &ValidatedCluster, + trino_role: &TrinoRole, + role_group_name: &RoleGroupName, + role_group_config: &TrinoRoleGroupConfig, + sa_name: &str, +) -> Result { + // Everything below is derived from the validated cluster and the validated role-group config, + // so the caller only needs to pass those (plus the applied ServiceAccount name). + let resolved_product_image = &cluster.image; + let trino_authentication_config = &cluster.cluster_config.authentication; + let catalogs = cluster.cluster_config.catalogs.as_slice(); + let resolved_fte_config = &cluster.cluster_config.fault_tolerant_execution; + let resolved_spooling_config = &cluster.cluster_config.client_protocol; + let trino_opa_config = &cluster.cluster_config.authorization; + let env_overrides = &role_group_config.env_overrides; + let merged_config = &role_group_config.config; + + let resource_names = cluster.resource_names(trino_role, role_group_name); + let config_map_name = resource_names.role_group_config_map().to_string(); + + let mut pod_builder = PodBuilder::new(); + + let prepare_container_name = Container::Prepare.to_string(); + let mut cb_prepare = ContainerBuilder::new(&prepare_container_name).with_context(|_| { + IllegalContainerNameSnafu { + container_name: prepare_container_name.clone(), + } + })?; + + let trino_container_name = Container::Trino.to_string(); + let mut cb_trino = ContainerBuilder::new(&trino_container_name).with_context(|_| { + IllegalContainerNameSnafu { + container_name: trino_container_name.clone(), + } + })?; + + // additional authentication env vars + let mut env = trino_authentication_config.env_vars(trino_role, &Container::Trino); + + let internal_secret_name = shared_internal_secret_name(&cluster.name); + env.push(env_var_from_secret( + &internal_secret_name, + None, + ENV_INTERNAL_SECRET, + )); + + let spooling_secret_name = shared_spooling_secret_name(&cluster.name); + env.push(env_var_from_secret( + &spooling_secret_name, + None, + ENV_SPOOLING_SECRET, + )); + + trino_authentication_config + .add_authentication_pod_and_volume_config( + trino_role, + &mut pod_builder, + &mut cb_prepare, + &mut cb_trino, + ) + .context(InvalidAuthenticationConfigSnafu)?; + build::graceful_shutdown::add_graceful_shutdown_config( + cluster, + trino_role, + merged_config, + &mut pod_builder, + &mut cb_trino, + ) + .context(GracefulShutdownSnafu)?; + + // Add the needed stuff for catalogs + env.extend( + catalogs + .iter() + .flat_map(|catalog| &catalog.env_bindings) + .cloned(), + ); + + // Needed by the `containerdebug` process to log it's tracing information to. + // This process runs in the background of the `trino` container. + // See command::container_trino_args() for how it's called. + env.push(EnvVar { + name: "CONTAINERDEBUG_LOG_DIRECTORY".into(), + value: Some(format!("{STACKABLE_LOG_DIR}/containerdebug")), + ..EnvVar::default() + }); + + // Finally add the user defined envOverrides properties. + env.extend(env_overrides.clone()); + + let requested_secret_lifetime = merged_config + .requested_secret_lifetime + .context(MissingSecretLifetimeSnafu)?; + + // add volume mounts depending on the client tls, internal tls, catalogs and authentication + tls_volume_mounts( + cluster, + trino_role, + &mut pod_builder, + &mut cb_prepare, + &mut cb_trino, + &requested_secret_lifetime, + )?; + + let mut prepare_args = vec![]; + if let ValidatedContainerLogConfigChoice::Automatic(log_config) = + &merged_config.logging.prepare_container + { + prepare_args.push(product_logging::framework::capture_shell_output( + STACKABLE_LOG_DIR, + &prepare_container_name, + log_config, + )); + } + + prepare_args.extend(command::container_prepare_args( + cluster, + catalogs, + merged_config, + resolved_fte_config, + resolved_spooling_config, + )); + + prepare_args + .extend(trino_authentication_config.commands(&TrinoRole::Coordinator, &Container::Prepare)); + + // Add OPA TLS certificate to truststore if configured + if let Some(tls_mount_path) = trino_opa_config + .as_ref() + .and_then(|opa_config| opa_config.tls_mount_path()) + { + prepare_args.extend(command::add_cert_to_truststore( + format!("{}/ca.crt", tls_mount_path).as_str(), + STACKABLE_CLIENT_TLS_DIR, + )); + } + + let container_prepare = cb_prepare + .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![prepare_args.join("\n")]) + .add_volume_mount(&*RW_CONFIG_VOLUME_NAME, RW_CONFIG_DIR_NAME) + .context(AddVolumeMountSnafu)? + .add_volume_mount(&*LOG_CONFIG_VOLUME_NAME, STACKABLE_LOG_CONFIG_DIR) + .context(AddVolumeMountSnafu)? + .add_volume_mount(&*LOG_VOLUME_NAME, STACKABLE_LOG_DIR) + .context(AddVolumeMountSnafu)? + .resources( + ResourceRequirementsBuilder::new() + .with_cpu_request("500m") + .with_cpu_limit("2000m") + .with_memory_request("4Gi") + .with_memory_limit("4Gi") + .build(), + ) + .build(); + + let mut persistent_volume_claims = vec![]; + // Add listener + if let Some(group_listener_name) = group_listener_name(cluster, trino_role) { + cb_trino + .add_volume_mount(&*LISTENER_VOLUME_NAME, LISTENER_VOLUME_DIR) + .context(AddVolumeMountSnafu)?; + + // Used for PVC templates that cannot be modified once they are deployed, so a fixed + // "none" version is used while keeping the other recommended labels. + let unversioned_recommended_labels = + cluster.unversioned_recommended_labels(trino_role, role_group_name); + + persistent_volume_claims.push( + build_group_listener_pvc(&group_listener_name, &unversioned_recommended_labels) + .context(ListenerConfigurationSnafu)?, + ); + } + + let container_trino = cb_trino + .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![ + command::container_trino_args(trino_authentication_config, catalogs).join("\n"), + ]) + .add_env_vars(env) + .add_volume_mount(&*CONFIG_VOLUME_NAME, CONFIG_DIR_NAME) + .context(AddVolumeMountSnafu)? + .add_volume_mount(&*RW_CONFIG_VOLUME_NAME, RW_CONFIG_DIR_NAME) + .context(AddVolumeMountSnafu)? + .add_volume_mount( + &*CATALOG_VOLUME_NAME, + format!("{}/catalog", CONFIG_DIR_NAME), + ) + .context(AddVolumeMountSnafu)? + .add_volume_mount(&*LOG_VOLUME_NAME, STACKABLE_LOG_DIR) + .context(AddVolumeMountSnafu)? + .add_container_ports(container_ports(cluster)) + .resources(merged_config.resources.clone().into()) + // The probes are set on coordinators and workers + .startup_probe(startup_probe(cluster)) + .readiness_probe(readiness_probe(cluster)) + .liveness_probe(liveness_probe(cluster)) + .build(); + + // add trino container first to better default into that container (e.g. instead of vector) + pod_builder.add_container(container_trino); + + // add password-update container if required + trino_authentication_config.add_authentication_containers(trino_role, &mut pod_builder); + + // The log-config volume mounts either the rolegroup ConfigMap (which carries the automatic + // `log.properties`) or a user-provided custom ConfigMap, depending on the validated choice. + let log_config_volume_config_map = match &merged_config.logging.trino_container { + ValidatedContainerLogConfigChoice::Custom(config_map) => config_map.to_string(), + ValidatedContainerLogConfigChoice::Automatic(_) => config_map_name.clone(), + }; + pod_builder + .add_volume(Volume { + name: LOG_CONFIG_VOLUME_NAME.to_string(), + config_map: Some(ConfigMapVolumeSource { + name: log_config_volume_config_map, + ..ConfigMapVolumeSource::default() + }), + ..Volume::default() + }) + .context(AddVolumeSnafu)?; + + if let Some(vector_log_config) = &merged_config.logging.vector_container { + pod_builder.add_container(vector_container( + &VECTOR_CONTAINER_NAME, + resolved_product_image, + vector_log_config, + &resource_names, + &CONFIG_VOLUME_NAME, + &LOG_VOLUME_NAME, + EnvVarSet::new(), + )); + } + + let metadata = ObjectMetaBuilder::new() + .with_labels(cluster.recommended_labels(trino_role, role_group_name)) + .with_annotation( + // This is actually used by some kuttl tests (as they don't specify the container explicitly) + Annotation::try_from(("kubectl.kubernetes.io/default-container", "trino")) + .context(AnnotationBuildSnafu)?, + ) + .build(); + + pod_builder + .metadata(metadata) + .image_pull_secrets_from_product_image(resolved_product_image) + .affinity(&merged_config.affinity) + .add_init_container(container_prepare) + .add_volume(Volume { + name: CONFIG_VOLUME_NAME.to_string(), + config_map: Some(ConfigMapVolumeSource { + name: config_map_name.clone(), + ..ConfigMapVolumeSource::default() + }), + ..Volume::default() + }) + .context(AddVolumeSnafu)? + .add_empty_dir_volume(&*RW_CONFIG_VOLUME_NAME, None) + .context(AddVolumeSnafu)? + .add_volume(Volume { + name: CATALOG_VOLUME_NAME.to_string(), + config_map: Some(ConfigMapVolumeSource { + name: cluster.role_group_catalog_config_map_name(trino_role, role_group_name), + ..ConfigMapVolumeSource::default() + }), + ..Volume::default() + }) + .context(AddVolumeSnafu)? + .add_empty_dir_volume( + &*LOG_VOLUME_NAME, + Some(product_logging::framework::calculate_log_volume_size_limit( + &[MAX_TRINO_LOG_FILES_SIZE, MAX_PREPARE_LOG_FILE_SIZE], + )), + ) + .context(AddVolumeSnafu)? + .service_account_name(sa_name) + .security_context(PodSecurityContextBuilder::new().fs_group(1000).build()); + + let mut pod_template = pod_builder.build_template(); + // `pod_overrides` already carries the merged role + role-group overrides, so a single merge + // here applies both. + pod_template.merge_from(role_group_config.pod_overrides.clone()); + + let annotations = restarter_ignore_secret_annotations( + trino_authentication_config + .hot_reloaded_secrets() + .iter() + .cloned(), + ); + + Ok(StatefulSet { + metadata: cluster + .object_meta( + resource_names.stateful_set_name().to_string(), + cluster.recommended_labels(trino_role, role_group_name), + ) + .with_label(RESTART_CONTROLLER_ENABLED_LABEL.to_owned()) + .with_annotations(annotations) + .build(), + spec: Some(StatefulSetSpec { + pod_management_policy: Some("Parallel".to_string()), + // Forward `None` when the user did not set `replicas`, leaving the field unset on the + // StatefulSet so a HorizontalPodAutoscaler can own the replica count. + replicas: role_group_config.replicas.map(i32::from), + selector: LabelSelector { + match_labels: Some( + cluster + .role_group_selector(trino_role, role_group_name) + .into(), + ), + ..LabelSelector::default() + }, + service_name: Some(resource_names.headless_service_name().to_string()), + template: pod_template, + volume_claim_templates: Some(persistent_volume_claims), + ..StatefulSetSpec::default() + }), + status: None, + }) +} + +/// Give a secret name and an optional key in the secret to use. +/// The value from the key will be set into the given env var name. +/// If not secret key is given, the env var name will be used as the secret key. +fn env_var_from_secret(secret_name: &str, secret_key: Option<&str>, env_var: &str) -> EnvVar { + EnvVar { + name: env_var.to_string(), + value_from: Some(EnvVarSource { + secret_key_ref: Some(SecretKeySelector { + optional: Some(false), + name: secret_name.to_string(), + key: secret_key.unwrap_or(env_var).to_string(), + }), + ..EnvVarSource::default() + }), + ..EnvVar::default() + } +} + +fn container_ports(cluster: &ValidatedCluster) -> 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() + }]; + + if cluster.server_tls_enabled() { + ports.push(ContainerPort { + name: Some(HTTPS_PORT_NAME.to_string()), + container_port: HTTPS_PORT.into(), + protocol: Some("TCP".to_string()), + ..ContainerPort::default() + }); + } else { + ports.push(ContainerPort { + name: Some(HTTP_PORT_NAME.to_string()), + container_port: HTTP_PORT.into(), + protocol: Some("TCP".to_string()), + ..ContainerPort::default() + }) + } + + ports +} + +fn startup_probe(cluster: &ValidatedCluster) -> Probe { + Probe { + exec: Some(finished_starting_probe(cluster)), + period_seconds: Some(5), + // Give the coordinator or worker 10 minutes to start up + failure_threshold: Some(120), + timeout_seconds: Some(3), + ..Default::default() + } +} + +fn readiness_probe(cluster: &ValidatedCluster) -> Probe { + Probe { + http_get: Some(http_get_probe(cluster)), + period_seconds: Some(5), + failure_threshold: Some(1), + timeout_seconds: Some(3), + ..Probe::default() + } +} + +fn liveness_probe(cluster: &ValidatedCluster) -> Probe { + Probe { + http_get: Some(http_get_probe(cluster)), + period_seconds: Some(5), + // Coordinators are currently not highly available, so you always have a singe instance. + // Restarting it causes all queries to fail, so let's not restart it directly after the first + // probe failure, but wait for 3 failures + // NOTE: This also applies to workers + failure_threshold: Some(3), + timeout_seconds: Some(3), + ..Probe::default() + } +} + +/// Check that `/v1/info` returns `200`. +/// +/// This is the same probe as the [upstream helm-chart](https://github.com/trinodb/charts/blob/7cd0a7bff6c52e0ee6ca6d5394cd72c150ad4379/charts/trino/templates/deployment-coordinator.yaml#L214) +/// is using. +fn http_get_probe(cluster: &ValidatedCluster) -> HTTPGetAction { + let (schema, port_name) = if cluster.server_tls_enabled() { + ("HTTPS", HTTPS_PORT_NAME) + } else { + ("HTTP", HTTP_PORT_NAME) + }; + + HTTPGetAction { + port: IntOrString::String(port_name.to_string()), + scheme: Some(schema.to_string()), + path: Some("/v1/info".to_string()), + ..Default::default() + } +} + +/// Wait until `/v1/info` returns `"starting":false`. +/// +/// This probe works on coordinators and workers. +fn finished_starting_probe(cluster: &ValidatedCluster) -> ExecAction { + let port = build::ports::exposed_port(cluster); + let schema = if cluster.server_tls_enabled() { + "https" + } else { + "http" + }; + + ExecAction { + command: Some(vec![ + "/bin/bash".to_string(), + "-x".to_string(), + "-euo".to_string(), + "pipefail".to_string(), + "-c".to_string(), + format!( + "curl --fail --insecure {schema}://127.0.0.1:{port}/v1/info | grep --silent '\\\"starting\\\":false'" + ), + ]), + } +} + +fn create_tls_volume( + volume_name: impl Into, + tls_secret_class: &SecretClassName, + requested_secret_lifetime: &Duration, + listener_scope: Option, +) -> Result { + let mut secret_volume_source_builder = SecretOperatorVolumeSourceBuilder::new( + tls_secret_class.as_ref(), + SecretClassVolumeProvisionParts::PublicPrivate, + ); + + secret_volume_source_builder + .with_pod_scope() + .with_format(SecretFormat::TlsPkcs12) + .with_tls_pkcs12_password(STACKABLE_TLS_STORE_PASSWORD) + .with_auto_tls_cert_lifetime(*requested_secret_lifetime); + + if let Some(listener_scope) = &listener_scope { + secret_volume_source_builder.with_listener_volume_scope(listener_scope); + } + + Ok(VolumeBuilder::new(volume_name) + .ephemeral( + secret_volume_source_builder + .build() + .context(TlsCertSecretClassVolumeBuildSnafu)?, + ) + .build()) +} + +fn tls_volume_mounts( + cluster: &ValidatedCluster, + trino_role: &TrinoRole, + pod_builder: &mut PodBuilder, + cb_prepare: &mut ContainerBuilder, + cb_trino: &mut ContainerBuilder, + requested_secret_lifetime: &Duration, +) -> Result<()> { + let catalogs = cluster.cluster_config.catalogs.as_slice(); + let resolved_fte_config = &cluster.cluster_config.fault_tolerant_execution; + let resolved_spooling_config = &cluster.cluster_config.client_protocol; + let trino_opa_config = &cluster.cluster_config.authorization; + + if let Some(server_tls) = cluster.get_server_tls() { + cb_prepare + .add_volume_mount( + &*SERVER_TLS_MOUNT_VOLUME_NAME, + STACKABLE_MOUNT_SERVER_TLS_DIR, + ) + .context(AddVolumeMountSnafu)?; + cb_trino + .add_volume_mount( + &*SERVER_TLS_MOUNT_VOLUME_NAME, + STACKABLE_MOUNT_SERVER_TLS_DIR, + ) + .context(AddVolumeMountSnafu)?; + pod_builder + .add_volume(create_tls_volume( + &*SERVER_TLS_MOUNT_VOLUME_NAME, + server_tls, + requested_secret_lifetime, + // add listener + secret_volume_listener_scope(trino_role), + )?) + .context(AddVolumeSnafu)?; + } + + cb_prepare + .add_volume_mount(&*SERVER_TLS_VOLUME_NAME, STACKABLE_SERVER_TLS_DIR) + .context(AddVolumeMountSnafu)?; + cb_trino + .add_volume_mount(&*SERVER_TLS_VOLUME_NAME, STACKABLE_SERVER_TLS_DIR) + .context(AddVolumeMountSnafu)?; + pod_builder + .add_empty_dir_volume(&*SERVER_TLS_VOLUME_NAME, None) + .context(AddVolumeSnafu)?; + + cb_prepare + .add_volume_mount(&*CLIENT_TLS_VOLUME_NAME, STACKABLE_CLIENT_TLS_DIR) + .context(AddVolumeMountSnafu)?; + cb_trino + .add_volume_mount(&*CLIENT_TLS_VOLUME_NAME, STACKABLE_CLIENT_TLS_DIR) + .context(AddVolumeMountSnafu)?; + pod_builder + .add_empty_dir_volume(&*CLIENT_TLS_VOLUME_NAME, None) + .context(AddVolumeSnafu)?; + + if let Some(internal_tls) = cluster.get_internal_tls() { + cb_prepare + .add_volume_mount( + &*INTERNAL_TLS_MOUNT_VOLUME_NAME, + STACKABLE_MOUNT_INTERNAL_TLS_DIR, + ) + .context(AddVolumeMountSnafu)?; + cb_trino + .add_volume_mount( + &*INTERNAL_TLS_MOUNT_VOLUME_NAME, + STACKABLE_MOUNT_INTERNAL_TLS_DIR, + ) + .context(AddVolumeMountSnafu)?; + pod_builder + .add_volume(create_tls_volume( + &*INTERNAL_TLS_MOUNT_VOLUME_NAME, + internal_tls, + requested_secret_lifetime, + None, + )?) + .context(AddVolumeSnafu)?; + + cb_prepare + .add_volume_mount(&*INTERNAL_TLS_VOLUME_NAME, STACKABLE_INTERNAL_TLS_DIR) + .context(AddVolumeMountSnafu)?; + cb_trino + .add_volume_mount(&*INTERNAL_TLS_VOLUME_NAME, STACKABLE_INTERNAL_TLS_DIR) + .context(AddVolumeMountSnafu)?; + pod_builder + .add_empty_dir_volume(&*INTERNAL_TLS_VOLUME_NAME, None) + .context(AddVolumeSnafu)?; + } + + // catalogs + for catalog in catalogs { + cb_prepare + .add_volume_mounts(catalog.volume_mounts.clone()) + .context(AddVolumeMountSnafu)?; + cb_trino + .add_volume_mounts(catalog.volume_mounts.clone()) + .context(AddVolumeMountSnafu)?; + pod_builder + .add_volumes(catalog.volumes.clone()) + .context(AddVolumeSnafu)?; + } + + // Add OPA TLS certs if configured + if let Some((tls_secret_class, tls_mount_path)) = + trino_opa_config.as_ref().and_then(|opa_config| { + opa_config + .tls_secret_class + .as_ref() + .zip(opa_config.tls_mount_path()) + }) + { + cb_prepare + .add_volume_mount(&*OPA_TLS_VOLUME_NAME, &tls_mount_path) + .context(AddVolumeMountSnafu)?; + + let opa_tls_volume = VolumeBuilder::new(&*OPA_TLS_VOLUME_NAME) + .ephemeral( + SecretOperatorVolumeSourceBuilder::new( + tls_secret_class.as_ref(), + SecretClassVolumeProvisionParts::PublicPrivate, + ) + .build() + .context(TlsCertSecretClassVolumeBuildSnafu)?, + ) + .build(); + + pod_builder + .add_volume(opa_tls_volume) + .context(AddVolumeSnafu)?; + } + + // fault tolerant execution S3 credentials and other resources + if let Some(resolved_fte) = resolved_fte_config { + cb_prepare + .add_volume_mounts(resolved_fte.volume_mounts.clone()) + .context(AddVolumeMountSnafu)?; + cb_trino + .add_volume_mounts(resolved_fte.volume_mounts.clone()) + .context(AddVolumeMountSnafu)?; + pod_builder + .add_volumes(resolved_fte.volumes.clone()) + .context(AddVolumeSnafu)?; + } + + // client spooling S3 credentials and other resources + if let Some(resolved_spooling) = resolved_spooling_config { + cb_prepare + .add_volume_mounts(resolved_spooling.volume_mounts.clone()) + .context(AddVolumeMountSnafu)?; + cb_trino + .add_volume_mounts(resolved_spooling.volume_mounts.clone()) + .context(AddVolumeMountSnafu)?; + pod_builder + .add_volumes(resolved_spooling.volumes.clone()) + .context(AddVolumeSnafu)?; + } + + Ok(()) +} diff --git a/rust/operator-binary/src/controller/dereference.rs b/rust/operator-binary/src/controller/dereference.rs index 81fed2d88..408657fb3 100644 --- a/rust/operator-binary/src/controller/dereference.rs +++ b/rust/operator-binary/src/controller/dereference.rs @@ -1,15 +1,14 @@ //! The dereference step in the TrinoCluster controller //! //! Fetches all Kubernetes objects referenced by the TrinoCluster spec and returns them in -//! [`DereferencedObjects`]. The functions called here (`CatalogConfig::from_catalog`, -//! `TrinoOpaConfig::from_opa_config`, `ResolvedFaultTolerantExecutionConfig::from_config`, -//! `ResolvedClientProtocolConfig::from_config`) currently mix fetching and validation; their -//! outputs are treated as "dereferenced" for now. Splitting those helpers is a follow-up. +//! [`DereferencedObjects`]. use std::{num::ParseIntError, str::FromStr}; -use snafu::{OptionExt, ResultExt, Snafu}; -use stackable_operator::{client::Client, kube::runtime::reflector::ObjectRef}; +use snafu::{ResultExt, Snafu}; +use stackable_operator::{ + client::Client, kube::runtime::reflector::ObjectRef, v2::controller_utils::get_namespace, +}; use crate::{ authorization::opa::TrinoOpaConfig, @@ -26,8 +25,10 @@ use crate::{ #[derive(Snafu, Debug)] pub enum Error { - #[snafu(display("object defines no namespace"))] - ObjectHasNoNamespace, + #[snafu(display("failed to get namespace"))] + GetNamespace { + source: stackable_operator::v2::controller_utils::Error, + }, #[snafu(display("failed to retrieve AuthenticationClass"))] AuthenticationClassRetrieval { @@ -67,8 +68,7 @@ pub enum Error { type Result = std::result::Result; -/// Kubernetes objects referenced from the TrinoCluster spec, already fetched (and, for now, partly -/// validated by the existing helper functions). +/// Kubernetes objects referenced from the TrinoCluster spec, fetched from the cluster. pub struct DereferencedObjects { pub resolved_authentication_classes: Vec, pub catalog_definitions: Vec, @@ -83,11 +83,7 @@ pub async fn dereference( client: &Client, trino: &v1alpha1::TrinoCluster, ) -> Result { - let namespace = trino - .metadata - .namespace - .as_deref() - .context(ObjectHasNoNamespaceSnafu)?; + let namespace = get_namespace(trino).context(GetNamespaceSnafu)?; let resolved_authentication_classes = resolve_authentication_classes(client, trino.get_authentication()) @@ -96,7 +92,7 @@ pub async fn dereference( let catalog_definitions = client .list_with_label_selector::( - namespace, + namespace.as_ref(), &trino.spec.cluster_config.catalog_label_selector, ) .await @@ -110,17 +106,18 @@ pub async fn dereference( let mut catalogs = Vec::with_capacity(catalog_definitions.len()); for catalog in &catalog_definitions { let catalog_ref = ObjectRef::from_obj(catalog); - let catalog_config = CatalogConfig::from_catalog(catalog, client, product_version) - .await - .context(ParseCatalogSnafu { - catalog: catalog_ref, - })?; + let catalog_config = + CatalogConfig::from_catalog(catalog, client, &namespace, product_version) + .await + .context(ParseCatalogSnafu { + catalog: catalog_ref, + })?; catalogs.push(catalog_config); } let trino_opa_config = match trino.get_opa_config() { Some(opa_config) => Some( - TrinoOpaConfig::from_opa_config(client, trino, opa_config) + TrinoOpaConfig::from_opa_config(client, trino, &namespace, opa_config) .await .context(InvalidOpaConfigSnafu)?, ), @@ -129,18 +126,26 @@ pub async fn dereference( let resolved_fte_config = match trino.spec.cluster_config.fault_tolerant_execution.as_ref() { Some(fte_config) => Some( - ResolvedFaultTolerantExecutionConfig::from_config(fte_config, Some(client), namespace) - .await - .context(FaultTolerantExecutionSnafu)?, + ResolvedFaultTolerantExecutionConfig::from_config( + fte_config, + Some(client), + namespace.as_ref(), + ) + .await + .context(FaultTolerantExecutionSnafu)?, ), None => None, }; let resolved_client_protocol_config = match trino.spec.cluster_config.client_protocol.as_ref() { Some(spooling_config) => Some( - ResolvedClientProtocolConfig::from_config(spooling_config, Some(client), namespace) - .await - .context(ClientProtocolConfigurationSnafu)?, + ResolvedClientProtocolConfig::from_config( + spooling_config, + Some(client), + namespace.as_ref(), + ) + .await + .context(ClientProtocolConfigurationSnafu)?, ), None => None, }; diff --git a/rust/operator-binary/src/controller/mod.rs b/rust/operator-binary/src/controller/mod.rs new file mode 100644 index 000000000..5c46f594c --- /dev/null +++ b/rust/operator-binary/src/controller/mod.rs @@ -0,0 +1,416 @@ +use std::{collections::BTreeMap, str::FromStr}; + +use stackable_operator::{ + builder::meta::ObjectMetaBuilder, + commons::{ + affinity::StackableAffinity, + product_image_selection::ResolvedProductImage, + resources::{NoRuntimeLimits, Resources}, + }, + kube::{Resource, api::ObjectMeta}, + kvp::Labels, + shared::time::Duration, + v2::{ + HasName, HasUid, NameIsValidLabelValue, + builder::meta::ownerreference_from_resource, + kvp::label::{recommended_labels, role_group_selector}, + role_group_utils::ResourceNames, + types::{ + kubernetes::{ListenerClassName, NamespaceName, SecretClassName, Uid}, + operator::{ + ClusterName, ControllerName, OperatorName, ProductName, ProductVersion, RoleName, + }, + }, + }, +}; + +use crate::{ + authentication::TrinoAuthenticationConfig, + authorization::opa::TrinoOpaConfig, + catalog::config::CatalogConfig, + config::{ + client_protocol::ResolvedClientProtocolConfig, + fault_tolerant_execution::ResolvedFaultTolerantExecutionConfig, + }, + crd::{APP_NAME, TrinoRole, discovery::TrinoPodRef, v1alpha1}, + trino_controller::{CONTROLLER_NAME, OPERATOR_NAME}, +}; + +pub(crate) mod build; +pub(crate) mod dereference; +pub(crate) mod validate; + +pub use validate::{RoleGroupName, TrinoRoleGroupConfig}; + +#[derive(Clone, Debug)] +pub struct ValidatedTls { + pub server: Option, + pub internal: Option, +} + +/// Cluster-wide settings, grouped to parallel `spec.clusterConfig` CRD. +#[derive(Clone, Debug)] +pub struct ValidatedClusterConfig { + pub tls: ValidatedTls, + pub authentication: TrinoAuthenticationConfig, + pub authorization: Option, + pub fault_tolerant_execution: Option, + pub client_protocol: Option, + pub coordinator_pod_refs: Vec, + pub catalogs: Vec, +} + +impl ValidatedClusterConfig { + /// Whether any authentication is configured. + pub fn authentication_enabled(&self) -> bool { + !self.authentication.is_empty() + } +} + +/// A validated, merged Trino role-group config. +/// +/// Holds the merged [`v1alpha1::TrinoConfig`] fields so the build steps consume this +/// controller-owned type instead of the raw CRD struct. +#[derive(Clone, Debug)] +pub struct ValidatedTrinoConfig { + pub affinity: StackableAffinity, + pub graceful_shutdown_timeout: Option, + pub logging: validate::ValidatedLogging, + pub query_max_memory: Option, + pub query_max_memory_per_node: Option, + pub resources: Resources, + pub requested_secret_lifetime: Option, +} + +impl ValidatedTrinoConfig { + /// Builds the validated config from the merged [`v1alpha1::TrinoConfig`], swapping in the + /// already-validated logging. + fn from_merged(merged: v1alpha1::TrinoConfig, logging: validate::ValidatedLogging) -> Self { + Self { + affinity: merged.affinity, + graceful_shutdown_timeout: merged.graceful_shutdown_timeout, + logging, + query_max_memory: merged.query_max_memory, + query_max_memory_per_node: merged.query_max_memory_per_node, + resources: merged.resources, + requested_secret_lifetime: merged.requested_secret_lifetime, + } + } +} + +/// Per-role configuration extracted during validation. +/// +/// Lets the reconciler and build steps consume this controller-owned type instead of re-reading +/// the raw [`v1alpha1::TrinoCluster`]. +#[derive(Clone, Debug)] +pub struct ValidatedRoleConfig { + pub pdb: stackable_operator::commons::pdb::PdbConfig, + /// The listener class for the role's group listener, if it has one (coordinator only). + pub listener_class: Option, +} + +/// The validated TrinoCluster. The output of the validate step. +#[derive(Clone, Debug)] +pub struct ValidatedCluster { + /// Metadata mirroring the source [`v1alpha1::TrinoCluster`] (name, namespace and UID). + /// + /// Kept private and only exposed through the [`Resource`] implementation, so that a + /// `ValidatedCluster` can be used directly as the owner of generated objects (e.g. to set + /// owner references) without threading the raw `TrinoCluster` through the build step. + metadata: ObjectMeta, + pub name: ClusterName, + pub namespace: NamespaceName, + pub uid: Uid, + pub image: ResolvedProductImage, + pub product_version: u16, + pub cluster_config: ValidatedClusterConfig, + 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, + image: ResolvedProductImage, + product_version: u16, + cluster_config: ValidatedClusterConfig, + role_configs: BTreeMap, + role_group_configs: BTreeMap>, + ) -> Self { + Self { + metadata: ObjectMeta { + name: Some(name.to_string()), + namespace: Some(namespace.to_string()), + uid: Some(uid.to_string()), + ..ObjectMeta::default() + }, + name, + namespace, + uid, + image, + product_version, + cluster_config, + role_configs, + role_group_configs, + } + } + + /// The validated per-role config for `role`, if the role is defined. + pub(crate) fn role_config(&self, role: &TrinoRole) -> Option<&ValidatedRoleConfig> { + self.role_configs.get(role) + } + + /// Whether the (client-facing) server TLS is enabled. + pub fn server_tls_enabled(&self) -> bool { + self.cluster_config.tls.server.is_some() + } + + /// Whether internal (inter-node) TLS is enabled. + pub fn internal_tls_enabled(&self) -> bool { + self.cluster_config.tls.internal.is_some() + } + + /// The user-provided server TLS SecretClass, if any. + pub fn get_server_tls(&self) -> Option<&SecretClassName> { + self.cluster_config.tls.server.as_ref() + } + + /// The user-provided internal TLS SecretClass, if any. + pub fn get_internal_tls(&self) -> Option<&SecretClassName> { + self.cluster_config.tls.internal.as_ref() + } + + /// Whether client TLS should be set, depending on authentication and server TLS settings. + pub fn tls_enabled(&self) -> bool { + self.cluster_config.authentication_enabled() || self.server_tls_enabled() + } + + /// Type-safe names for the resources of a given role group. + pub(crate) fn resource_names( + &self, + role: &TrinoRole, + role_group_name: &RoleGroupName, + ) -> ResourceNames { + ResourceNames { + cluster_name: self.name.clone(), + role_name: Self::role_name(role), + role_group_name: role_group_name.clone(), + } + } + + /// Name of the rolegroup's catalog [`ConfigMap`], derived from the rolegroup config map name + /// by appending the `-catalog` suffix. + pub(crate) fn role_group_catalog_config_map_name( + &self, + role: &TrinoRole, + role_group_name: &RoleGroupName, + ) -> String { + format!( + "{}-catalog", + self.resource_names(role, role_group_name) + .role_group_config_map() + ) + } + + /// Returns an [`ObjectMetaBuilder`] pre-filled with this cluster's namespace, an owner + /// reference back to the cluster, the resource `name` and the given `recommended_labels`. + /// + /// Consolidates the metadata chain repeated by the child-resource builders. Call sites that + /// need extra labels/annotations chain them onto the returned builder. + pub(crate) fn object_meta( + &self, + name: impl Into, + recommended_labels: Labels, + ) -> ObjectMetaBuilder { + let mut builder = ObjectMetaBuilder::new(); + builder + .name_and_namespace(self) + .name(name) + .ownerreference(ownerreference_from_resource(self, None, Some(true))) + .with_labels(recommended_labels); + builder + } + + /// A [`TrinoRole`] as a type-safe [`RoleName`]. + fn role_name(role: &TrinoRole) -> RoleName { + RoleName::from_str(&role.to_string()).expect("a TrinoRole is a valid RFC 1123 role name") + } + + /// The version label value (`app.kubernetes.io/version`) as a type-safe [`ProductVersion`]. + fn version_label(&self) -> ProductVersion { + ProductVersion::from_str(&self.image.app_version_label_value) + .expect("the app version label value is a valid product version") + } + + fn recommended_labels_with_version( + &self, + version: &ProductVersion, + role: &TrinoRole, + role_group_name: &RoleGroupName, + ) -> Labels { + recommended_labels( + self, + &product_name(), + version, + &operator_name(), + &controller_name(), + &Self::role_name(role), + role_group_name, + ) + } + + /// Recommended labels for a role-group resource (using the resolved product version). + pub(crate) fn recommended_labels( + &self, + role: &TrinoRole, + role_group_name: &RoleGroupName, + ) -> Labels { + self.recommended_labels_with_version(&self.version_label(), role, role_group_name) + } + + /// Recommended labels using a fixed `"none"` version, for resources whose labels must not + /// change after creation (e.g. listener PVC templates). + pub(crate) fn unversioned_recommended_labels( + &self, + role: &TrinoRole, + role_group_name: &RoleGroupName, + ) -> Labels { + let none = ProductVersion::from_str("none") + .expect("\"none\" is a valid product version label value"); + self.recommended_labels_with_version(&none, role, role_group_name) + } + + /// Selector labels matching the pods of a role group. + pub(crate) fn role_group_selector( + &self, + role: &TrinoRole, + role_group_name: &RoleGroupName, + ) -> Labels { + role_group_selector( + self, + &product_name(), + &Self::role_name(role), + role_group_name, + ) + } +} + +impl Resource for ValidatedCluster { + type DynamicType = ::DynamicType; + type Scope = ::Scope; + + fn kind(dt: &Self::DynamicType) -> std::borrow::Cow<'_, str> { + v1alpha1::TrinoCluster::kind(dt) + } + + fn group(dt: &Self::DynamicType) -> std::borrow::Cow<'_, str> { + v1alpha1::TrinoCluster::group(dt) + } + + fn version(dt: &Self::DynamicType) -> std::borrow::Cow<'_, str> { + v1alpha1::TrinoCluster::version(dt) + } + + fn plural(dt: &Self::DynamicType) -> std::borrow::Cow<'_, str> { + v1alpha1::TrinoCluster::plural(dt) + } + + fn meta(&self) -> &ObjectMeta { + &self.metadata + } + + fn meta_mut(&mut self) -> &mut ObjectMeta { + &mut self.metadata + } +} + +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 (`trino`) as a type-safe label value. +pub(crate) fn product_name() -> ProductName { + ProductName::from_str(APP_NAME).expect("'trino' 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(CONTROLLER_NAME).expect("the controller name is a valid label value") +} + +/// A minimal, valid TrinoCluster spec shared across unit tests. +#[cfg(test)] +pub(crate) const MINIMAL_TRINO_YAML: &str = r#" + apiVersion: trino.stackable.tech/v1alpha1 + kind: TrinoCluster + metadata: + name: simple-trino + namespace: default + uid: "e6ac237d-a6d4-43a1-8135-f36506110912" + spec: + image: + productVersion: "479" + clusterConfig: + catalogLabelSelector: {} + coordinators: + roleGroups: + default: + replicas: 1 + workers: + roleGroups: + default: + replicas: 1 + "#; + +/// Parses [`MINIMAL_TRINO_YAML`] into a [`v1alpha1::TrinoCluster`]. +#[cfg(test)] +pub(crate) fn minimal_trino() -> v1alpha1::TrinoCluster { + serde_yaml::from_str(MINIMAL_TRINO_YAML).expect("invalid test TrinoCluster YAML") +} + +/// The validated [`MINIMAL_TRINO_YAML`] cluster with empty dereferenced inputs. The common test +/// fixture for build-step unit tests. +#[cfg(test)] +pub(crate) fn validated_cluster() -> ValidatedCluster { + use stackable_operator::cli::OperatorEnvironmentOptions; + + use crate::controller::dereference::DereferencedObjects; + + let derefs = DereferencedObjects { + resolved_authentication_classes: Vec::new(), + catalog_definitions: Vec::new(), + catalogs: Vec::new(), + trino_opa_config: None, + resolved_fte_config: None, + resolved_client_protocol_config: None, + }; + let operator_env = OperatorEnvironmentOptions { + operator_namespace: "stackable-operators".to_string(), + operator_service_name: "trino-operator".to_string(), + image_repository: "oci.example.org".to_string(), + }; + + validate::validate(&minimal_trino(), &derefs, &operator_env).expect("validate should succeed") +} diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 8d16b7874..d9f7adee1 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -1,30 +1,39 @@ -//! The validate step in the TrinoCluster controller +//! The validate step in the TrinoCluster controller. //! -//! Synchronously validates inputs that don't require a Kubernetes client. Produces -//! [`ValidatedInputs`], consumed by the rest of `reconcile_trino`. +//! Synchronously validates inputs that don't require a Kubernetes client and +//! produces the typed [`ValidatedCluster`], consumed by `controller::build::*`. -use std::collections::HashMap; +use std::{collections::BTreeMap, str::FromStr}; -use product_config::{ProductConfigManager, types::PropertyNameKind}; -use snafu::{ResultExt, Snafu}; +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, + kube::ResourceExt as _, + product_logging::spec::Logging, + role_utils::{GenericRoleConfig, RoleGroup}, + 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, RoleGroupConfig, with_validated_config}, + types::kubernetes::ConfigMapName, }, }; -use strum::{EnumDiscriminants, IntoStaticStr}; +use strum::{EnumDiscriminants, IntoEnumIterator, IntoStaticStr}; +use super::{ + ValidatedCluster, ValidatedClusterConfig, ValidatedRoleConfig, ValidatedTls, + ValidatedTrinoConfig, +}; use crate::{ authentication::{self, TrinoAuthenticationConfig, TrinoAuthenticationTypes}, controller::dereference::DereferencedObjects, - crd::{ - ACCESS_CONTROL_PROPERTIES, CONFIG_PROPERTIES, EXCHANGE_MANAGER_PROPERTIES, JVM_CONFIG, - JVM_SECURITY_PROPERTIES, LOG_PROPERTIES, NODE_PROPERTIES, SPOOLING_MANAGER_PROPERTIES, - TrinoRole, v1alpha1, - }, + crd::{Container, TrinoRole, v1alpha1}, }; #[derive(Snafu, Debug, EnumDiscriminants)] @@ -36,6 +45,27 @@ pub enum Error { source: product_image_selection::Error, }, + #[snafu(display("failed to get the cluster name"))] + GetClusterName { + source: stackable_operator::v2::controller_utils::Error, + }, + + #[snafu(display("failed to get the cluster namespace"))] + GetClusterNamespace { + source: stackable_operator::v2::controller_utils::Error, + }, + + #[snafu(display("failed to get the cluster UID"))] + GetClusterUid { + source: stackable_operator::v2::controller_utils::Error, + }, + + #[snafu(display("unable to parse Trino version {product_version:?}"))] + ParseTrinoVersion { + source: std::num::ParseIntError, + product_version: String, + }, + #[snafu(display("unsupported Trino authentication"))] UnsupportedAuthenticationConfig { source: authentication::Error }, @@ -53,45 +83,112 @@ pub enum Error { role: String, }, - #[snafu(display("failed to transform configs"))] - ProductConfigTransform { - source: stackable_operator::product_config_utils::Error, + #[snafu(display("invalid role group name {role_group}"))] + ParseRoleGroupName { + source: stackable_operator::v2::macros::attributed_string_type::Error, + role_group: String, }, - #[snafu(display("invalid product config"))] - InvalidProductConfig { - source: stackable_operator::product_config_utils::Error, + #[snafu(display("failed to resolve and merge config for role group {role_group}"))] + FailedToResolveConfig { + source: fragment::ValidationError, + role_group: RoleGroupName, }, + + #[snafu(display("invalid environment variable override name"))] + ParseEnvVarName { source: container::Error }, + + #[snafu(display("failed to validate 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; -/// Synchronous inputs the rest of `reconcile_trino` needs after dereferencing. -pub struct ValidatedInputs { - pub image: ResolvedProductImage, - pub trino_authentication_config: TrinoAuthenticationConfig, - pub validated_role_config: ValidatedRoleConfigByPropertyKind, +pub use stackable_operator::v2::types::operator::RoleGroupName; + +/// Validated logging configuration for the Trino, prepare 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, Eq)] +pub struct ValidatedLogging { + pub prepare_container: ValidatedContainerLogConfigChoice, + pub trino_container: ValidatedContainerLogConfigChoice, + pub vector_container: Option, + pub enable_vector_agent: bool, } -/// Validates the cluster spec and the dereferenced inputs. +/// Validates the logging configuration for the Trino, prepare and (optional) Vector containers. +/// +/// `vector_aggregator_config_map_name` is the discovery ConfigMap name of the Vector aggregator; +/// it is required (and validated) only when the Vector agent is enabled. +fn validate_logging( + logging: &Logging, + vector_aggregator_config_map_name: &Option, +) -> Result { + let prepare_container = + validate_logging_configuration_for_container(logging, &Container::Prepare) + .context(ValidateLoggingConfigSnafu)?; + let trino_container = validate_logging_configuration_for_container(logging, &Container::Trino) + .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, &Container::Vector) + .context(ValidateLoggingConfigSnafu)?, + vector_aggregator_config_map_name, + }) + } else { + None + }; + + Ok(ValidatedLogging { + prepare_container, + trino_container, + vector_container, + enable_vector_agent: logging.enable_vector_agent, + }) +} + +pub type TrinoRoleGroupConfig = + RoleGroupConfig; + +/// Validates the cluster spec and dereferenced inputs. pub fn validate( trino: &v1alpha1::TrinoCluster, dereferenced_objects: &DereferencedObjects, operator_environment: &OperatorEnvironmentOptions, - product_config: &ProductConfigManager, -) -> Result { - let resolved_product_image = trino +) -> Result { + let namespace = get_namespace(trino).context(GetClusterNamespaceSnafu)?; + + let image = trino .spec .image .resolve( - super::CONTAINER_IMAGE_BASE_NAME, + crate::trino_controller::CONTAINER_IMAGE_BASE_NAME, &operator_environment.image_repository, crate::built_info::PKG_VERSION, ) .context(ResolveProductImageSnafu)?; - let trino_authentication_config = TrinoAuthenticationConfig::new( - &resolved_product_image, + let product_version = + u16::from_str(&image.product_version).context(ParseTrinoVersionSnafu { + product_version: image.product_version.clone(), + })?; + + let authentication = TrinoAuthenticationConfig::new( + &image, TrinoAuthenticationTypes::try_from( dereferenced_objects.resolved_authentication_classes.clone(), ) @@ -102,77 +199,413 @@ pub fn validate( if dereferenced_objects .resolved_client_protocol_config .is_some() - && resolved_product_image.product_version.starts_with("45") + && image.product_version.starts_with("45") { return Err(Error::ClientSpoolingProtocolTrinoVersion { - product_version: resolved_product_image.product_version.clone(), + product_version: image.product_version.clone(), }); } - let validated_role_config = validated_product_config( - trino, - // The Trino version is a single number like 396. - // The product config expects semver formatted version strings. - // That is why we just add minor and patch version 0 here. - &format!("{}.0.0", resolved_product_image.product_version), - product_config, - )?; - - Ok(ValidatedInputs { - image: resolved_product_image, - trino_authentication_config, - validated_role_config, + // The Vector aggregator discovery ConfigMap name (validated here so an invalid name fails + // up-front). It is only required when the Vector agent is enabled for a role group. + let vector_aggregator_config_map_name = trino + .spec + .cluster_config + .vector_aggregator_config_map_name + .clone(); + + let mut role_configs: BTreeMap = BTreeMap::new(); + let mut role_group_configs: BTreeMap> = + BTreeMap::new(); + for trino_role in TrinoRole::iter() { + let role = trino + .role(&trino_role) + .with_context(|_| MissingTrinoRoleSnafu { + role: trino_role.to_string(), + })?; + + // Extract the per-role PDB and (optional) listener class up-front, so the reconciler and + // build steps consume the validated config instead of re-reading the raw cluster. + role_configs.insert( + trino_role.clone(), + ValidatedRoleConfig { + pdb: trino + .generic_role_config(&trino_role) + .map(|rc| rc.pod_disruption_budget.clone()) + .unwrap_or_default(), + listener_class: trino_role.listener_class_name(trino), + }, + ); + + let default_config = v1alpha1::TrinoConfig::default_config( + &trino.name_any(), + &trino_role, + &dereferenced_objects.catalog_definitions, + ); + let mut groups = BTreeMap::new(); + for (rg_name, rg) in &role.role_groups { + let role_group_name = + RoleGroupName::from_str(rg_name).with_context(|_| ParseRoleGroupNameSnafu { + role_group: rg_name.clone(), + })?; + // Merges and validates the role group config (default <- role <- role group). Because + // `JavaCommonConfig` implements `Merge`, the role and role-group `jvmArgumentOverrides` + // are merged here too and carried by `product_specific_common_config`. + let merged = with_validated_config::< + v1alpha1::TrinoConfig, + JavaCommonConfig, + v1alpha1::TrinoConfigFragment, + GenericRoleConfig, + v1alpha1::TrinoConfigOverrides, + >(rg, &role, &default_config) + .with_context(|_| FailedToResolveConfigSnafu { + role_group: role_group_name.clone(), + })?; + groups.insert( + role_group_name, + into_role_group_config(merged, &vector_aggregator_config_map_name)?, + ); + } + role_group_configs.insert(trino_role, groups); + } + + let tls = &trino.spec.cluster_config.tls; + let cluster_config = ValidatedClusterConfig { + tls: ValidatedTls { + server: tls.server_secret_class.clone(), + internal: tls.internal_secret_class.clone(), + }, + authentication, + authorization: dereferenced_objects.trino_opa_config.clone(), + fault_tolerant_execution: dereferenced_objects.resolved_fte_config.clone(), + client_protocol: dereferenced_objects.resolved_client_protocol_config.clone(), + coordinator_pod_refs: trino.coordinator_pods(&namespace).collect(), + catalogs: dereferenced_objects.catalogs.clone(), + }; + + let name = get_cluster_name(trino).context(GetClusterNameSnafu)?; + let uid = get_uid(trino).context(GetClusterUidSnafu)?; + + Ok(ValidatedCluster::new( + name, + namespace, + uid, + image, + product_version, + cluster_config, + role_configs, + role_group_configs, + )) +} + +/// Adapts the validated [`RoleGroup`] produced by [`with_validated_config`] into the flattened +/// [`TrinoRoleGroupConfig`] consumed by the build steps. +fn into_role_group_config( + merged: RoleGroup, + vector_aggregator_config_map_name: &Option, +) -> Result { + let replicas = merged.replicas; + let common = merged.config; + + let mut env_overrides = EnvVarSet::new(); + for (name, value) in common.env_overrides { + env_overrides = env_overrides.with_value( + &EnvVarName::from_str(&name).context(ParseEnvVarNameSnafu)?, + value, + ); + } + + let logging = validate_logging(&common.config.logging, vector_aggregator_config_map_name)?; + + Ok(RoleGroupConfig { + replicas, + config: ValidatedTrinoConfig::from_merged(common.config, logging), + config_overrides: common.config_overrides, + env_overrides, + cli_overrides: common.cli_overrides, + pod_overrides: common.pod_overrides, + product_specific_common_config: common.product_specific_common_config, }) } -pub(super) fn validated_product_config( +/// Test-only helper: merges and validates a single role group's config from an arbitrary +/// [`v1alpha1::TrinoCluster`] (with the given catalogs feeding `default_config`), reusing the +/// production merge path. Shared by the `crd::affinity` and `config::jvm` unit tests, which need a +/// merged config without dereferencing a full cluster. +#[cfg(test)] +pub(crate) fn merged_role_group_config( trino: &v1alpha1::TrinoCluster, - version: &str, - product_config: &ProductConfigManager, -) -> Result { - let mut roles = HashMap::new(); - - let config_files = vec![ - PropertyNameKind::Env, - PropertyNameKind::File(CONFIG_PROPERTIES.to_string()), - PropertyNameKind::File(NODE_PROPERTIES.to_string()), - PropertyNameKind::File(JVM_CONFIG.to_string()), - PropertyNameKind::File(LOG_PROPERTIES.to_string()), - PropertyNameKind::File(JVM_SECURITY_PROPERTIES.to_string()), - PropertyNameKind::File(ACCESS_CONTROL_PROPERTIES.to_string()), - PropertyNameKind::File(SPOOLING_MANAGER_PROPERTIES.to_string()), - PropertyNameKind::File(EXCHANGE_MANAGER_PROPERTIES.to_string()), - ]; - - let coordinator_role = TrinoRole::Coordinator; - roles.insert( - coordinator_role.to_string(), - ( - config_files.clone(), - trino - .role(&coordinator_role) - .with_context(|_| MissingTrinoRoleSnafu { - role: coordinator_role.to_string(), - })?, - ), - ); - - let worker_role = TrinoRole::Worker; - roles.insert( - worker_role.to_string(), - ( - config_files, - trino - .role(&worker_role) - .with_context(|_| MissingTrinoRoleSnafu { - role: worker_role.to_string(), - })?, - ), - ); - - let role_config = - transform_all_roles_to_config(trino, &roles).context(ProductConfigTransformSnafu)?; - - validate_all_roles_and_groups_config(version, &role_config, product_config, false, false) - .context(InvalidProductConfigSnafu) + trino_role: &TrinoRole, + role_group: &str, + trino_catalogs: &[crate::crd::catalog::v1alpha1::TrinoCatalog], +) -> TrinoRoleGroupConfig { + let role = trino.role(trino_role).expect("role should be defined"); + let default_config = + v1alpha1::TrinoConfig::default_config(&trino.name_any(), trino_role, trino_catalogs); + let rg = role + .role_groups + .get(role_group) + .expect("role group should be defined"); + let merged = with_validated_config::< + v1alpha1::TrinoConfig, + JavaCommonConfig, + v1alpha1::TrinoConfigFragment, + GenericRoleConfig, + v1alpha1::TrinoConfigOverrides, + >(rg, &role, &default_config) + .expect("role group config should be valid"); + // The shared test clusters do not enable the Vector agent, so no aggregator ConfigMap name is + // required here. + into_role_group_config(merged, &None).expect("env overrides should be valid") +} + +#[cfg(test)] +mod tests { + use std::{collections::BTreeMap, str::FromStr}; + + use stackable_operator::cli::OperatorEnvironmentOptions; + + use super::{Error, RoleGroupName, validate}; + use crate::{ + config::client_protocol::ResolvedClientProtocolConfig, + controller::{ValidatedCluster, dereference::DereferencedObjects, validated_cluster}, + crd::{TrinoRole, v1alpha1}, + }; + + fn empty_derefs() -> DereferencedObjects { + DereferencedObjects { + resolved_authentication_classes: Vec::new(), + catalog_definitions: Vec::new(), + catalogs: Vec::new(), + trino_opa_config: None, + resolved_fte_config: None, + resolved_client_protocol_config: None, + } + } + + fn validate_yaml( + yaml: &str, + derefs: &DereferencedObjects, + ) -> std::result::Result { + let trino: v1alpha1::TrinoCluster = serde_yaml::from_str(yaml).expect("invalid test YAML"); + let operator_env = OperatorEnvironmentOptions { + operator_namespace: "stackable-operators".to_string(), + operator_service_name: "trino-operator".to_string(), + image_repository: "oci.example.org".to_string(), + }; + validate(&trino, derefs, &operator_env) + } + + /// Minimal cluster YAML with both roles defined, parameterised on the product version. + fn minimal_yaml(product_version: &str) -> String { + format!( + r#" + apiVersion: trino.stackable.tech/v1alpha1 + kind: TrinoCluster + metadata: + name: simple-trino + namespace: default + uid: "e6ac237d-a6d4-43a1-8135-f36506110912" + spec: + image: + productVersion: "{product_version}" + clusterConfig: + catalogLabelSelector: {{}} + coordinators: + roleGroups: + default: + replicas: 1 + workers: + roleGroups: + default: + replicas: 1 + "# + ) + } + + #[test] + fn validate_minimal_cluster() { + let validated = validated_cluster(); + + assert_eq!(validated.name.to_string(), "simple-trino"); + assert_eq!(validated.namespace.to_string(), "default"); + assert_eq!( + validated.uid.to_string(), + "e6ac237d-a6d4-43a1-8135-f36506110912" + ); + assert_eq!(validated.product_version, 479); + assert!(!validated.cluster_config.authentication_enabled()); + assert!( + validated + .role_group_configs + .contains_key(&TrinoRole::Coordinator) + ); + assert!( + validated + .role_group_configs + .contains_key(&TrinoRole::Worker) + ); + assert_eq!( + validated.role_group_configs[&TrinoRole::Coordinator] + [&RoleGroupName::from_str("default").unwrap()] + .replicas, + Some(1) + ); + } + + #[test] + fn validate_logging_rejects_invalid_custom_config_map_name() { + let yaml = r#" + apiVersion: trino.stackable.tech/v1alpha1 + kind: TrinoCluster + metadata: + name: simple-trino + namespace: default + uid: "e6ac237d-a6d4-43a1-8135-f36506110912" + spec: + image: + productVersion: "479" + clusterConfig: + catalogLabelSelector: {} + coordinators: + config: + logging: + containers: + trino: + custom: + configMap: "invalid ConfigMap name" + roleGroups: + default: + replicas: 1 + workers: + roleGroups: + default: + replicas: 1 + "#; + + let err = validate_yaml(yaml, &empty_derefs()).unwrap_err(); + assert!(matches!(err, Error::ValidateLoggingConfig { .. })); + } + + #[test] + fn validate_logging_requires_vector_aggregator_when_agent_enabled() { + // The Vector agent is enabled but no aggregator discovery ConfigMap name is provided. + let yaml = r#" + apiVersion: trino.stackable.tech/v1alpha1 + kind: TrinoCluster + metadata: + name: simple-trino + namespace: default + uid: "e6ac237d-a6d4-43a1-8135-f36506110912" + spec: + image: + productVersion: "479" + clusterConfig: + catalogLabelSelector: {} + coordinators: + config: + logging: + enableVectorAgent: true + roleGroups: + default: + replicas: 1 + workers: + roleGroups: + default: + replicas: 1 + "#; + + let err = validate_yaml(yaml, &empty_derefs()).unwrap_err(); + assert!(matches!(err, Error::MissingVectorAggregatorConfigMapName)); + } + + #[test] + fn spooling_protocol_rejected_on_trino_45() { + let mut derefs = empty_derefs(); + derefs.resolved_client_protocol_config = Some(ResolvedClientProtocolConfig { + config_properties: BTreeMap::new(), + spooling_manager_properties: BTreeMap::new(), + volumes: Vec::new(), + volume_mounts: Vec::new(), + init_container_extra_start_commands: Vec::new(), + }); + + let err = validate_yaml(&minimal_yaml("451"), &derefs).unwrap_err(); + assert!(matches!( + err, + Error::ClientSpoolingProtocolTrinoVersion { .. } + )); + } + + #[test] + fn spooling_protocol_accepted_on_newer_trino() { + let mut derefs = empty_derefs(); + derefs.resolved_client_protocol_config = Some(ResolvedClientProtocolConfig { + config_properties: BTreeMap::new(), + spooling_manager_properties: BTreeMap::new(), + volumes: Vec::new(), + volume_mounts: Vec::new(), + init_container_extra_start_commands: Vec::new(), + }); + + assert!(validate_yaml(&minimal_yaml("479"), &derefs).is_ok()); + } + + #[test] + fn rejects_invalid_env_override_name() { + let yaml = r#" + apiVersion: trino.stackable.tech/v1alpha1 + kind: TrinoCluster + metadata: + name: simple-trino + namespace: default + uid: "e6ac237d-a6d4-43a1-8135-f36506110912" + spec: + image: + productVersion: "479" + clusterConfig: + catalogLabelSelector: {} + coordinators: + roleGroups: + default: + replicas: 1 + envOverrides: + "BAD=NAME": "value" + workers: + roleGroups: + default: + replicas: 1 + "#; + + let err = validate_yaml(yaml, &empty_derefs()).unwrap_err(); + assert!(matches!(err, Error::ParseEnvVarName { .. })); + } + + #[test] + fn rejects_invalid_role_group_name() { + let yaml = r#" + apiVersion: trino.stackable.tech/v1alpha1 + kind: TrinoCluster + metadata: + name: simple-trino + namespace: default + uid: "e6ac237d-a6d4-43a1-8135-f36506110912" + spec: + image: + productVersion: "479" + clusterConfig: + catalogLabelSelector: {} + coordinators: + roleGroups: + "Invalid_Name": + replicas: 1 + workers: + roleGroups: + default: + replicas: 1 + "#; + + let err = validate_yaml(yaml, &empty_derefs()).unwrap_err(); + assert!(matches!(err, Error::ParseRoleGroupName { .. })); + } } diff --git a/rust/operator-binary/src/crd/affinity.rs b/rust/operator-binary/src/crd/affinity.rs index bac46c9ad..75545b4bf 100644 --- a/rust/operator-binary/src/crd/affinity.rs +++ b/rust/operator-binary/src/crd/affinity.rs @@ -39,7 +39,7 @@ pub fn get_affinity( .map(|hive_cluster_name| { affinity_between_role_pods( "hive", - hive_cluster_name, // The discovery cm has the same name as the HiveCluster itself + hive_cluster_name.as_ref(), // The discovery cm has the same name as the HiveCluster itself "metastore", 50, ) @@ -67,7 +67,7 @@ pub fn get_affinity( .map(|hdfs_cluster_name| { affinity_between_role_pods( "hdfs", - hdfs_cluster_name, // The discovery cm has the same name as the HdfsCluster itself + hdfs_cluster_name.as_ref(), // The discovery cm has the same name as the HdfsCluster itself "datanode", 50, ) @@ -136,9 +136,9 @@ mod tests { "#; let trino: v1alpha1::TrinoCluster = serde_yaml::from_str(input).expect("illegal test input"); - let merged_config = trino - .merged_config(&role, &role.rolegroup_ref(&trino, "default"), &[]) - .unwrap(); + let merged_config = + crate::controller::validate::merged_role_group_config(&trino, &role, "default", &[]) + .config; assert_eq!( merged_config.affinity, @@ -278,13 +278,13 @@ mod tests { let hive_catalog_2: catalog::v1alpha1::TrinoCatalog = serde_yaml::with::singleton_map_recursive::deserialize(deserializer).unwrap(); - let merged_config = trino - .merged_config( - &role, - &role.rolegroup_ref(&trino, "default"), - &[hive_catalog_1, tpch_catalog, hive_catalog_2], - ) - .unwrap(); + let merged_config = crate::controller::validate::merged_role_group_config( + &trino, + &role, + "default", + &[hive_catalog_1, tpch_catalog, hive_catalog_2], + ) + .config; let mut expected_affinities = vec![WeightedPodAffinityTerm { pod_affinity_term: PodAffinityTerm { diff --git a/rust/operator-binary/src/crd/catalog/commons.rs b/rust/operator-binary/src/crd/catalog/commons.rs index 39fe834cc..a4fe7c2ef 100644 --- a/rust/operator-binary/src/crd/catalog/commons.rs +++ b/rust/operator-binary/src/crd/catalog/commons.rs @@ -1,16 +1,19 @@ use serde::{Deserialize, Serialize}; -use stackable_operator::schemars::{self, JsonSchema}; +use stackable_operator::{ + schemars::{self, JsonSchema}, + v2::types::kubernetes::ConfigMapName, +}; #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct MetastoreConnection { /// Name of the [discovery ConfigMap](DOCS_BASE_URL_PLACEHOLDER/concepts/service_discovery) providing information about the Hive metastore. - pub config_map: String, + pub config_map: ConfigMapName, } #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct HdfsConnection { /// Name of the [discovery ConfigMap](DOCS_BASE_URL_PLACEHOLDER/concepts/service_discovery) providing information about the HDFS cluster. - pub config_map: String, + pub config_map: ConfigMapName, } diff --git a/rust/operator-binary/src/crd/client_protocol.rs b/rust/operator-binary/src/crd/client_protocol.rs index 19d4dcae9..4a899e5ce 100644 --- a/rust/operator-binary/src/crd/client_protocol.rs +++ b/rust/operator-binary/src/crd/client_protocol.rs @@ -1,5 +1,5 @@ -/// This module manages the client protocol properties, especially the for spooling. -/// Trino documentation is available here: https://trino.io/docs/current/client/client-protocol.html +//! This module manages the client protocol properties, especially for spooling. +//! Trino documentation is available here: use serde::{Deserialize, Serialize}; use stackable_operator::schemars::{self, JsonSchema}; diff --git a/rust/operator-binary/src/crd/discovery.rs b/rust/operator-binary/src/crd/discovery.rs index e1899fe46..0eb0ff996 100644 --- a/rust/operator-binary/src/crd/discovery.rs +++ b/rust/operator-binary/src/crd/discovery.rs @@ -1,4 +1,4 @@ -use stackable_operator::utils::cluster_info::KubernetesClusterInfo; +use stackable_operator::{utils::cluster_info::KubernetesClusterInfo, v2::types::common::Port}; use crate::crd::{HTTP_PORT, HTTPS_PORT}; @@ -55,7 +55,7 @@ pub enum TrinoDiscoveryProtocol { } impl TrinoDiscoveryProtocol { - pub fn port(&self) -> u16 { + pub fn port(&self) -> Port { match self { TrinoDiscoveryProtocol::Http => HTTP_PORT, TrinoDiscoveryProtocol::Https => HTTPS_PORT, diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index ae133dfcc..ec154f8af 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -5,11 +5,11 @@ pub mod client_protocol; pub mod discovery; pub mod fault_tolerant_execution; -use std::{collections::BTreeMap, ops::Div, str::FromStr}; +use std::{collections::BTreeMap, str::FromStr}; use affinity::get_affinity; use serde::{Deserialize, Serialize}; -use snafu::{OptionExt, ResultExt, Snafu}; +use snafu::{OptionExt, Snafu}; use stackable_operator::{ commons::{ affinity::StackableAffinity, @@ -21,28 +21,30 @@ use stackable_operator::{ Resources, ResourcesFragment, }, }, - config::{ - fragment::{self, Fragment, ValidationError}, - merge::Merge, - }, - config_overrides::{KeyValueConfigOverrides, KeyValueOverridesProvider}, + config::{fragment::Fragment, merge::Merge}, crd::authentication::core, deep_merger::ObjectOverrides, k8s_openapi::apimachinery::pkg::{api::resource::Quantity, apis::meta::v1::LabelSelector}, - kube::{CustomResource, ResourceExt, runtime::reflector::ObjectRef}, + kube::{CustomResource, ResourceExt}, memory::{BinaryMultiple, MemoryQuantity}, - product_config_utils::{Configuration, Error as ConfigError}, product_logging::{self, spec::Logging}, - role_utils::{ - CommonConfiguration, GenericRoleConfig, JavaCommonConfig, Role, RoleGroup, RoleGroupRef, - }, + role_utils::{GenericRoleConfig, Role}, schemars::{self, JsonSchema}, shared::time::Duration, status::condition::{ClusterCondition, HasStatusCondition}, - utils::cluster_info::KubernetesClusterInfo, + v2::{ + config_overrides::KeyValueConfigOverrides, + role_group_utils::ResourceNames, + role_utils::JavaCommonConfig, + types::{ + common::Port, + kubernetes::{ConfigMapName, ListenerClassName, NamespaceName, SecretClassName}, + operator::{ClusterName, RoleGroupName, RoleName}, + }, + }, versioned::versioned, }; -use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; +use strum::{Display, EnumIter, EnumString}; use crate::crd::discovery::TrinoPodRef; @@ -60,64 +62,22 @@ pub type TrinoRoleType = Role< JavaCommonConfig, >; -pub type TrinoRoleGroupType = - RoleGroup; - -pub const FIELD_MANAGER: &str = "trino-operator"; pub const APP_NAME: &str = "trino"; // ports -pub const HTTP_PORT: u16 = 8080; -pub const HTTPS_PORT: u16 = 8443; -pub const METRICS_PORT: u16 = 8081; +pub const HTTP_PORT: Port = Port(8080); +pub const HTTPS_PORT: Port = Port(8443); +pub const METRICS_PORT: Port = Port(8081); // port names pub const HTTP_PORT_NAME: &str = "http"; pub const HTTPS_PORT_NAME: &str = "https"; pub const METRICS_PORT_NAME: &str = "metrics"; -// file names -pub const CONFIG_PROPERTIES: &str = "config.properties"; -pub const JVM_CONFIG: &str = "jvm.config"; -pub const NODE_PROPERTIES: &str = "node.properties"; -pub const LOG_PROPERTIES: &str = "log.properties"; -pub const ACCESS_CONTROL_PROPERTIES: &str = "access-control.properties"; -pub const JVM_SECURITY_PROPERTIES: &str = "security.properties"; -pub const EXCHANGE_MANAGER_PROPERTIES: &str = "exchange-manager.properties"; -pub const SPOOLING_MANAGER_PROPERTIES: &str = "spooling-manager.properties"; -// node.properties -pub const NODE_ENVIRONMENT: &str = "node.environment"; -// config.properties -pub const COORDINATOR: &str = "coordinator"; -pub const DISCOVERY_URI: &str = "discovery.uri"; -pub const HTTP_SERVER_HTTP_PORT: &str = "http-server.http.port"; -pub const QUERY_MAX_MEMORY: &str = "query.max-memory"; -pub const QUERY_MAX_MEMORY_PER_NODE: &str = "query.max-memory-per-node"; -// - server tls -pub const HTTP_SERVER_HTTPS_PORT: &str = "http-server.https.port"; -pub const HTTP_SERVER_HTTPS_ENABLED: &str = "http-server.https.enabled"; -pub const HTTP_SERVER_HTTPS_KEYSTORE_KEY: &str = "http-server.https.keystore.key"; -pub const HTTP_SERVER_KEYSTORE_PATH: &str = "http-server.https.keystore.path"; -pub const HTTP_SERVER_HTTPS_TRUSTSTORE_KEY: &str = "http-server.https.truststore.key"; -pub const HTTP_SERVER_TRUSTSTORE_PATH: &str = "http-server.https.truststore.path"; -pub const HTTP_SERVER_AUTHENTICATION_ALLOW_INSECURE_OVER_HTTP: &str = - "http-server.authentication.allow-insecure-over-http"; -// - internal tls -pub const INTERNAL_COMMUNICATION_SHARED_SECRET: &str = "internal-communication.shared-secret"; -pub const INTERNAL_COMMUNICATION_HTTPS_KEYSTORE_PATH: &str = - "internal-communication.https.keystore.path"; -pub const INTERNAL_COMMUNICATION_HTTPS_KEYSTORE_KEY: &str = - "internal-communication.https.keystore.key"; -pub const INTERNAL_COMMUNICATION_HTTPS_TRUSTSTORE_PATH: &str = - "internal-communication.https.truststore.path"; -pub const INTERNAL_COMMUNICATION_HTTPS_TRUSTSTORE_KEY: &str = - "internal-communication.https.truststore.key"; -pub const NODE_INTERNAL_ADDRESS_SOURCE: &str = "node.internal-address-source"; -pub const NODE_INTERNAL_ADDRESS_SOURCE_FQDN: &str = "FQDN"; // directories pub const CONFIG_DIR_NAME: &str = "/stackable/config"; pub const RW_CONFIG_DIR_NAME: &str = "/stackable/rwconfig"; pub const STACKABLE_SERVER_TLS_DIR: &str = "/stackable/server_tls"; pub const STACKABLE_CLIENT_TLS_DIR: &str = "/stackable/client_tls"; pub const STACKABLE_INTERNAL_TLS_DIR: &str = "/stackable/internal_tls"; -pub const STACKABLE_LOG_DIR: &str = "/stackable/log"; +pub use stackable_operator::v2::product_logging::framework::STACKABLE_LOG_DIR; pub const STACKABLE_MOUNT_SERVER_TLS_DIR: &str = "/stackable/mount_server_tls"; pub const STACKABLE_MOUNT_INTERNAL_TLS_DIR: &str = "/stackable/mount_internal_tls"; // store pws @@ -126,34 +86,19 @@ pub const STACKABLE_TLS_STORE_PASSWORD: &str = "changeit"; pub const ENV_INTERNAL_SECRET: &str = "INTERNAL_SECRET"; pub const ENV_SPOOLING_SECRET: &str = "SPOOLING_SECRET"; // TLS -pub const TLS_DEFAULT_SECRET_CLASS: &str = "tls"; +const TLS_DEFAULT_SECRET_CLASS: &str = "tls"; +// Listener +pub const DEFAULT_LISTENER_CLASS: &str = "cluster-internal"; // Logging -pub const LOG_FORMAT: &str = "log.format"; -pub const LOG_PATH: &str = "log.path"; -pub const LOG_COMPRESSION: &str = "log.compression"; -pub const LOG_MAX_SIZE: &str = "log.max-size"; -pub const LOG_MAX_TOTAL_SIZE: &str = "log.max-total-size"; -const LOG_FILE_COUNT: u32 = 2; pub const MAX_TRINO_LOG_FILES_SIZE: MemoryQuantity = MemoryQuantity { value: 10.0, unit: BinaryMultiple::Mebi, }; -pub const JVM_HEAP_FACTOR: f32 = 0.8; - -pub const DEFAULT_COORDINATOR_GRACEFUL_SHUTDOWN_TIMEOUT: Duration = +const DEFAULT_COORDINATOR_GRACEFUL_SHUTDOWN_TIMEOUT: Duration = Duration::from_minutes_unchecked(15); pub const DEFAULT_WORKER_GRACEFUL_SHUTDOWN_TIMEOUT: Duration = Duration::from_minutes_unchecked(60); -/// Corresponds to "shutdown.grace-period", which defaults to 2 min. -/// This seems a bit high, as Pod termination - even with no queries running on the worker - -/// takes at least 4 minutes (see ). -/// So we set it to 30 seconds, so the Pod termination takes at least 1 minute. -pub const WORKER_SHUTDOWN_GRACE_PERIOD: Duration = Duration::from_secs(30); - -/// Safety puffer to guarantee the graceful shutdown works every time. -pub const WORKER_GRACEFUL_SHUTDOWN_SAFETY_OVERHEAD: Duration = Duration::from_secs(10); - /// Convert a Kubernetes `Quantity` to a Trino property string in bytes, e.g. `"65536B"`. pub(crate) fn quantity_to_trino_bytes( q: &Quantity, @@ -165,27 +110,8 @@ pub(crate) fn quantity_to_trino_bytes( #[derive(Snafu, Debug)] pub enum Error { - #[snafu(display("object has no namespace associated"))] - NoNamespace, - - #[snafu(display("object has no names"))] - NoName, - - #[snafu(display("unknown role {role}. Should be one of {roles:?}"))] - UnknownTrinoRole { - source: strum::ParseError, - role: String, - roles: Vec, - }, - #[snafu(display("the role {role} is not defined"))] CannotRetrieveTrinoRole { role: String }, - - #[snafu(display("the role group {role_group} is not defined"))] - CannotRetrieveTrinoRoleGroup { role_group: String }, - - #[snafu(display("fragment validation failure"))] - FragmentValidationFailure { source: ValidationError }, } #[versioned( @@ -237,69 +163,47 @@ pub mod versioned { pub workers: Option, } - #[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] + #[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] - pub struct TrinoConfigOverrides { - #[serde( - default, - rename = "config.properties", - skip_serializing_if = "Option::is_none" - )] - pub config_properties: Option, + pub struct TrinoCoordinatorRoleConfig { + #[serde(flatten)] + pub common: GenericRoleConfig, - #[serde( - default, - rename = "node.properties", - skip_serializing_if = "Option::is_none" - )] - pub node_properties: Option, + /// This field controls which [ListenerClass](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listenerclass.html) is used to expose the coordinator. + #[serde(default = "coordinator_default_listener_class")] + pub listener_class: ListenerClassName, + } - #[serde( - default, - rename = "log.properties", - skip_serializing_if = "Option::is_none" - )] - pub log_properties: Option, + #[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, Merge, PartialEq, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct TrinoConfigOverrides { + // File name defined in [`crate::controller::build::properties::ConfigFileName`] + #[serde(default, rename = "config.properties")] + pub config_properties: KeyValueConfigOverrides, - #[serde( - default, - rename = "security.properties", - skip_serializing_if = "Option::is_none" - )] - pub security_properties: Option, + // File name defined in [`crate::controller::build::properties::ConfigFileName`] + #[serde(default, rename = "node.properties")] + pub node_properties: KeyValueConfigOverrides, - #[serde( - default, - rename = "access-control.properties", - skip_serializing_if = "Option::is_none" - )] - pub access_control_properties: Option, + // File name defined in [`crate::controller::build::properties::ConfigFileName`] + #[serde(default, rename = "log.properties")] + pub log_properties: KeyValueConfigOverrides, - #[serde( - default, - rename = "exchange-manager.properties", - skip_serializing_if = "Option::is_none" - )] - pub exchange_manager_properties: Option, + // File name defined in [`crate::controller::build::properties::ConfigFileName`] + #[serde(default, rename = "security.properties")] + pub security_properties: KeyValueConfigOverrides, - #[serde( - default, - rename = "spooling-manager.properties", - skip_serializing_if = "Option::is_none" - )] - pub spooling_manager_properties: Option, - } + // File name defined in [`crate::controller::build::properties::ConfigFileName`] + #[serde(default, rename = "access-control.properties")] + pub access_control_properties: KeyValueConfigOverrides, - // TODO: move generic version to op-rs? - #[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] - #[serde(rename_all = "camelCase")] - pub struct TrinoCoordinatorRoleConfig { - #[serde(flatten)] - pub common: GenericRoleConfig, + // File name defined in [`crate::controller::build::properties::ConfigFileName`] + #[serde(default, rename = "exchange-manager.properties")] + pub exchange_manager_properties: KeyValueConfigOverrides, - /// This field controls which [ListenerClass](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listenerclass.html) is used to expose the coordinator. - #[serde(default = "coordinator_default_listener_class")] - pub listener_class: String, + // File name defined in [`crate::controller::build::properties::ConfigFileName`] + #[serde(default, rename = "spooling-manager.properties")] + pub spooling_manager_properties: KeyValueConfigOverrides, } #[derive(Clone, Debug, Default, Fragment, JsonSchema, PartialEq)] @@ -317,26 +221,29 @@ pub mod versioned { serde(rename_all = "camelCase") )] pub struct TrinoConfig { - // config.properties - pub query_max_memory: Option, + #[fragment_attrs(serde(default))] + pub affinity: StackableAffinity, - pub query_max_memory_per_node: Option, + /// Time period Pods have to gracefully shut down, e.g. `30m`, `1h` or `2d`. Consult the operator documentation for details. + #[fragment_attrs(serde(default))] + pub graceful_shutdown_timeout: Option, #[fragment_attrs(serde(default))] pub logging: Logging, + /// This is the max amount of user memory a query can use across the entire cluster. + /// See + pub query_max_memory: Option, + + /// This is the max amount of user memory a query can use on a worker. + /// See + pub query_max_memory_per_node: Option, + // We need to provide *something* that implements `Fragment`, so we pick an empty struct here. // Note that a unit "()" would not work, as we need something that implements `Fragment`. #[fragment_attrs(serde(default))] pub resources: Resources, - #[fragment_attrs(serde(default))] - pub affinity: StackableAffinity, - - /// Time period Pods have to gracefully shut down, e.g. `30m`, `1h` or `2d`. Consult the operator documentation for details. - #[fragment_attrs(serde(default))] - pub graceful_shutdown_timeout: Option, - /// Request secret (currently only autoTls certificates) lifetime from the secret operator, e.g. `7d`, or `30d`. /// This can be shortened by the `maxCertificateLifetime` setting on the SecretClass issuing the TLS certificate. /// @@ -382,7 +289,7 @@ 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, } #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] @@ -418,7 +325,7 @@ pub mod versioned { default = "tls_secret_class_default", skip_serializing_if = "Option::is_none" )] - pub server_secret_class: Option, + pub server_secret_class: Option, /// Only affects internal communication. Use mutual verification between Trino nodes /// This setting controls: /// - Which cert the servers should use to authenticate themselves against other servers @@ -427,7 +334,7 @@ pub mod versioned { default = "tls_secret_class_default", skip_serializing_if = "Option::is_none" )] - pub internal_secret_class: Option, + pub internal_secret_class: Option, } #[derive(Clone, Debug, Default, JsonSchema, PartialEq, Fragment)] @@ -460,24 +367,6 @@ impl v1alpha1::TrinoAuthorizationOpaConfig { } } -impl KeyValueOverridesProvider for v1alpha1::TrinoConfigOverrides { - fn get_key_value_overrides(&self, file: &str) -> BTreeMap> { - let field = match file { - CONFIG_PROPERTIES => self.config_properties.as_ref(), - NODE_PROPERTIES => self.node_properties.as_ref(), - LOG_PROPERTIES => self.log_properties.as_ref(), - JVM_SECURITY_PROPERTIES => self.security_properties.as_ref(), - ACCESS_CONTROL_PROPERTIES => self.access_control_properties.as_ref(), - EXCHANGE_MANAGER_PROPERTIES => self.exchange_manager_properties.as_ref(), - SPOOLING_MANAGER_PROPERTIES => self.spooling_manager_properties.as_ref(), - _ => None, - }; - field - .map(KeyValueConfigOverrides::as_product_config_overrides) - .unwrap_or_default() - } -} - impl Default for v1alpha1::TrinoCoordinatorRoleConfig { fn default() -> Self { v1alpha1::TrinoCoordinatorRoleConfig { @@ -487,8 +376,9 @@ impl Default for v1alpha1::TrinoCoordinatorRoleConfig { } } -fn coordinator_default_listener_class() -> String { - "cluster-internal".to_string() +fn coordinator_default_listener_class() -> ListenerClassName { + ListenerClassName::from_str(DEFAULT_LISTENER_CLASS) + .expect("the default listener class name must be valid") } impl Default for v1alpha1::TrinoTls { @@ -500,8 +390,11 @@ impl Default for v1alpha1::TrinoTls { } } -fn tls_secret_class_default() -> Option { - Some(TLS_DEFAULT_SECRET_CLASS.to_string()) +fn tls_secret_class_default() -> Option { + Some( + SecretClassName::from_str(TLS_DEFAULT_SECRET_CLASS) + .expect("the default TLS SecretClass name must be valid"), + ) } #[derive( @@ -513,7 +406,9 @@ fn tls_secret_class_default() -> Option { Eq, Hash, JsonSchema, + Ord, PartialEq, + PartialOrd, Serialize, EnumString, )] @@ -525,37 +420,7 @@ pub enum TrinoRole { } impl TrinoRole { - /// Returns the container start command for a Trino node. - pub fn get_command(&self) -> Vec { - vec![ - "bin/launcher".to_string(), - "run".to_string(), - format!("--etc-dir={}", CONFIG_DIR_NAME), - ] - } - - /// Metadata about a rolegroup - pub fn rolegroup_ref( - &self, - trino: &v1alpha1::TrinoCluster, - group_name: impl Into, - ) -> RoleGroupRef { - RoleGroupRef { - cluster: ObjectRef::from_obj(trino), - role: self.to_string(), - role_group: group_name.into(), - } - } - - pub fn roles() -> Vec { - let mut roles = vec![]; - for role in Self::iter() { - roles.push(role.to_string()) - } - roles - } - - pub fn listener_class_name(&self, trino: &v1alpha1::TrinoCluster) -> Option { + pub fn listener_class_name(&self, trino: &v1alpha1::TrinoCluster) -> Option { match self { Self::Coordinator => trino .spec @@ -595,7 +460,7 @@ pub enum Container { } impl v1alpha1::TrinoConfig { - fn default_config( + pub(crate) fn default_config( cluster_name: &str, role: &TrinoRole, trino_catalogs: &[catalog::v1alpha1::TrinoCatalog], @@ -638,235 +503,7 @@ impl v1alpha1::TrinoConfig { } } -impl Configuration for v1alpha1::TrinoConfigFragment { - type Configurable = v1alpha1::TrinoCluster; - - fn compute_env( - &self, - _resource: &Self::Configurable, - _role_name: &str, - ) -> Result>, ConfigError> { - Ok(BTreeMap::new()) - } - - fn compute_cli( - &self, - _resource: &Self::Configurable, - _role_name: &str, - ) -> Result>, ConfigError> { - Ok(BTreeMap::new()) - } - - fn compute_files( - &self, - resource: &Self::Configurable, - role_name: &str, - file: &str, - ) -> Result>, ConfigError> { - let mut result = BTreeMap::new(); - let authentication_enabled = resource.authentication_enabled(); - let server_tls_enabled: bool = resource.get_server_tls().is_some(); - let internal_tls_enabled: bool = resource.get_internal_tls().is_some(); - - match file { - NODE_PROPERTIES => { - // The resource name is alphanumeric and may have "-" characters - // The Trino node environment is bound to alphanumeric lowercase and "_" characters - // and must start with alphanumeric (which is the case for resource names as well?) - // see https://trino.io/docs/current/installation/deployment.html - let node_env = resource.name_any().to_ascii_lowercase().replace('-', "_"); - result.insert(NODE_ENVIRONMENT.to_string(), Some(node_env)); - } - CONFIG_PROPERTIES => { - // coordinator or worker - result.insert( - COORDINATOR.to_string(), - Some((role_name == TrinoRole::Coordinator.to_string()).to_string()), - ); - // TrinoConfig properties - if let Some(query_max_memory) = &self.query_max_memory { - result.insert( - QUERY_MAX_MEMORY.to_string(), - Some(query_max_memory.to_string()), - ); - } - if let Some(query_max_memory_per_node) = &self.query_max_memory_per_node { - result.insert( - QUERY_MAX_MEMORY_PER_NODE.to_string(), - Some(query_max_memory_per_node.to_string()), - ); - } - - // The log format used by Trino - result.insert(LOG_FORMAT.to_string(), Some("json".to_string())); - // The path to the log file used by Trino - result.insert( - LOG_PATH.to_string(), - Some(format!( - "{STACKABLE_LOG_DIR}/{container}/server.airlift.json", - container = Container::Trino - )), - ); - - // We do not compress. This will result in LOG_MAX_TOTAL_SIZE / LOG_MAX_SIZE files. - result.insert(LOG_COMPRESSION.to_string(), Some("none".to_string())); - - // The size of one log file - result.insert( - LOG_MAX_SIZE.to_string(), - Some(format!( - // Trino uses the unit "MB" for MiB. - "{}MB", - MAX_TRINO_LOG_FILES_SIZE - .scale_to(BinaryMultiple::Mebi) - .div(LOG_FILE_COUNT as f32) - .ceil() - .value, - )), - ); - // The maximum size of all logfiles combined - result.insert( - LOG_MAX_TOTAL_SIZE.to_string(), - Some(format!( - // Trino uses the unit "MB" for MiB. - "{}MB", - MAX_TRINO_LOG_FILES_SIZE - .scale_to(BinaryMultiple::Mebi) - .ceil() - .value, - )), - ); - - // disable http-request logs - result.insert( - "http-server.log.enabled".to_string(), - Some("false".to_string()), - ); - - // Always use the internal secret (base64) - result.insert( - INTERNAL_COMMUNICATION_SHARED_SECRET.to_string(), - Some(format!("${{ENV:{secret}}}", secret = ENV_INTERNAL_SECRET)), - ); - - // If authentication is enabled and client tls is explicitly deactivated we error out - // Therefore from here on we can use resource.get_server_tls() as the only source - // of truth when enabling client TLS. - if authentication_enabled && !server_tls_enabled { - return Err(ConfigError::InvalidProductSpecificConfiguration { - reason: - "Trino requires client TLS to be enabled if any authentication method is enabled! TLS was set to null. \ - Please set 'spec.clusterConfig.tls.secretClass' or use the provided default value.".to_string(), - }); - } - - if server_tls_enabled || internal_tls_enabled { - // enable TLS - result.insert( - HTTP_SERVER_HTTPS_ENABLED.to_string(), - Some(true.to_string()), - ); - // via https port - result.insert( - HTTP_SERVER_HTTPS_PORT.to_string(), - Some(HTTPS_PORT.to_string()), - ); - - let tls_store_dir = if server_tls_enabled { - STACKABLE_SERVER_TLS_DIR - } else { - // allow insecure communication - result.insert( - HTTP_SERVER_AUTHENTICATION_ALLOW_INSECURE_OVER_HTTP.to_string(), - Some("true".to_string()), - ); - // via the http port - result.insert( - HTTP_SERVER_HTTP_PORT.to_string(), - Some(HTTP_PORT.to_string()), - ); - - STACKABLE_INTERNAL_TLS_DIR - }; - - result.insert( - HTTP_SERVER_KEYSTORE_PATH.to_string(), - Some(format!("{}/{}", tls_store_dir, "keystore.p12")), - ); - result.insert( - HTTP_SERVER_HTTPS_KEYSTORE_KEY.to_string(), - Some(STACKABLE_TLS_STORE_PASSWORD.to_string()), - ); - result.insert( - HTTP_SERVER_TRUSTSTORE_PATH.to_string(), - Some(format!("{}/{}", tls_store_dir, "truststore.p12")), - ); - result.insert( - HTTP_SERVER_HTTPS_TRUSTSTORE_KEY.to_string(), - Some(STACKABLE_TLS_STORE_PASSWORD.to_string()), - ); - } - - if internal_tls_enabled { - result.insert( - INTERNAL_COMMUNICATION_HTTPS_KEYSTORE_PATH.to_string(), - Some(format!("{}/keystore.p12", STACKABLE_INTERNAL_TLS_DIR)), - ); - result.insert( - INTERNAL_COMMUNICATION_HTTPS_KEYSTORE_KEY.to_string(), - Some(STACKABLE_TLS_STORE_PASSWORD.to_string()), - ); - result.insert( - INTERNAL_COMMUNICATION_HTTPS_TRUSTSTORE_PATH.to_string(), - Some(format!("{}/truststore.p12", STACKABLE_INTERNAL_TLS_DIR)), - ); - result.insert( - INTERNAL_COMMUNICATION_HTTPS_TRUSTSTORE_KEY.to_string(), - Some(STACKABLE_TLS_STORE_PASSWORD.to_string()), - ); - result.insert( - NODE_INTERNAL_ADDRESS_SOURCE.to_string(), - Some(NODE_INTERNAL_ADDRESS_SOURCE_FQDN.to_string()), - ); - } - } - LOG_PROPERTIES => {} - ACCESS_CONTROL_PROPERTIES => {} - _ => {} - } - - Ok(result) - } -} - impl v1alpha1::TrinoCluster { - /// Returns the name of the cluster and raises an Error if the name is not set. - pub fn name_r(&self) -> Result { - self.metadata.name.to_owned().context(NoNameSnafu) - } - - /// Returns the namespace of the cluster and raises an Error if the name is not set. - pub fn namespace_r(&self) -> Result { - self.metadata.namespace.to_owned().context(NoNamespaceSnafu) - } - - pub fn role_service_name(&self, role: &TrinoRole) -> Result { - Ok(format!("{}-{}", self.name_r()?, role)) - } - - pub fn role_service_fqdn( - &self, - role: &TrinoRole, - cluster_info: &KubernetesClusterInfo, - ) -> Result { - Ok(format!( - "{role_service_name}.{namespace}.svc.{cluster_domain}", - role_service_name = self.role_service_name(role)?, - namespace = self.namespace_r()?, - cluster_domain = cluster_info.cluster_domain - )) - } - /// Returns a reference to the role. Raises an error if the role is not defined. pub fn role(&self, role_variant: &TrinoRole) -> Result { match role_variant { @@ -882,28 +519,6 @@ impl v1alpha1::TrinoCluster { }) } - /// Returns a reference to the role group. Raises an error if the role or role group are not defined. - pub fn rolegroup( - &self, - rolegroup_ref: &RoleGroupRef, - ) -> Result { - let trino_role = - TrinoRole::from_str(&rolegroup_ref.role).with_context(|_| UnknownTrinoRoleSnafu { - role: rolegroup_ref.role.to_owned(), - roles: TrinoRole::roles(), - })?; - - let role_variant = self.role(&trino_role)?; - - role_variant - .role_groups - .get(&rolegroup_ref.role_group) - .cloned() - .with_context(|| CannotRetrieveTrinoRoleGroupSnafu { - role_group: rolegroup_ref.role_group.to_owned(), - }) - } - pub fn generic_role_config(&self, role: &TrinoRole) -> Option<&GenericRoleConfig> { match role { TrinoRole::Coordinator => self @@ -915,46 +530,20 @@ impl v1alpha1::TrinoCluster { } } - pub fn num_workers(&self) -> u16 { - self.spec - .workers - .iter() - .flat_map(|w| w.role_groups.values()) - .map(|rg| rg.replicas.unwrap_or(1)) - .sum() - } - - /// Returns the minimal gracefulShutdownTimeout of all the worker rolegroups. - pub fn min_worker_graceful_shutdown_timeout(&self) -> Duration { - let role_timeout = self - .spec - .workers - .as_ref() - .and_then(|w| w.config.config.graceful_shutdown_timeout); - self.spec - .workers - .as_ref() - .iter() - .flat_map(|worker| worker.role_groups.values()) - .map(|role_group| { - role_group - .config - .config - .graceful_shutdown_timeout - .unwrap_or(role_timeout.unwrap_or(DEFAULT_WORKER_GRACEFUL_SHUTDOWN_TIMEOUT)) - }) - .min() - .unwrap_or(DEFAULT_WORKER_GRACEFUL_SHUTDOWN_TIMEOUT) - } - /// List all coordinator pods expected to form the cluster /// /// We try to predict the pods here rather than looking at the current cluster state in order to /// avoid instance churn. - pub fn coordinator_pods(&self) -> Result + '_, Error> { - let ns = self.metadata.namespace.clone().context(NoNamespaceSnafu)?; - Ok(self - .spec + pub fn coordinator_pods( + &self, + namespace: &NamespaceName, + ) -> impl Iterator + '_ { + let ns = namespace.to_string(); + let cluster_name = ClusterName::from_str(&self.name_any()) + .expect("a TrinoCluster name is a valid cluster name"); + let role_name = RoleName::from_str(&TrinoRole::Coordinator.to_string()) + .expect("the coordinator role name is a valid role name"); + self.spec .coordinators .iter() .flat_map(|role| &role.role_groups) @@ -962,17 +551,19 @@ impl v1alpha1::TrinoCluster { .collect::>() .into_iter() .flat_map(move |(rolegroup_name, rolegroup)| { - let role_group_ref = TrinoRole::Coordinator.rolegroup_ref(self, rolegroup_name); + let resource_names = ResourceNames { + cluster_name: cluster_name.clone(), + role_name: role_name.clone(), + role_group_name: RoleGroupName::from_str(rolegroup_name) + .expect("a role group name is a valid role group name"), + }; let ns = ns.clone(); (0..rolegroup.replicas.unwrap_or(0)).map(move |i| TrinoPodRef { namespace: ns.clone(), - role_group_service_name: role_group_ref.rolegroup_headless_service_name(), - pod_name: format!( - "{role_group}-{i}", - role_group = role_group_ref.object_name() - ), + role_group_service_name: resource_names.headless_service_name().to_string(), + pod_name: format!("{}-{i}", resource_names.stateful_set_name()), }) - })) + }) } /// Returns user provided authentication settings @@ -980,12 +571,6 @@ impl v1alpha1::TrinoCluster { &self.spec.cluster_config.authentication } - /// Check if any authentication settings are provided - pub fn authentication_enabled(&self) -> bool { - let spec: &v1alpha1::TrinoClusterSpec = &self.spec; - !spec.cluster_config.authentication.is_empty() - } - pub fn get_opa_config(&self) -> Option<&v1alpha1::TrinoAuthorizationOpaConfig> { self.spec .cluster_config @@ -995,111 +580,16 @@ impl v1alpha1::TrinoCluster { v1alpha1::TrinoAuthorization::Opa { config } => config, }) } - - /// Return user provided server TLS settings - pub fn get_server_tls(&self) -> Option<&str> { - let spec: &v1alpha1::TrinoClusterSpec = &self.spec; - spec.cluster_config.tls.server_secret_class.as_deref() - } - - /// Return if client TLS should be set depending on settings for authentication and client TLS. - pub fn tls_enabled(&self) -> bool { - self.authentication_enabled() || self.get_server_tls().is_some() - } - - /// Return user provided internal TLS settings. - pub fn get_internal_tls(&self) -> Option<&str> { - let spec: &v1alpha1::TrinoClusterSpec = &self.spec; - spec.cluster_config.tls.internal_secret_class.as_deref() - } - - pub fn exposed_port(&self) -> u16 { - match self.get_server_tls() { - Some(_) => HTTPS_PORT, - None => HTTP_PORT, - } - } - - pub fn exposed_protocol(&self) -> &str { - match self.get_server_tls() { - Some(_) => HTTPS_PORT_NAME, - None => HTTP_PORT_NAME, - } - } - - /// Returns if the HTTP port should be exposed - pub fn expose_http_port(&self) -> bool { - self.get_server_tls().is_none() - } - - /// Returns if the HTTPS port should be exposed - pub fn expose_https_port(&self) -> bool { - self.get_server_tls().is_some() - } - - /// Retrieve and merge resource configs for role and role groups - pub fn merged_config( - &self, - role: &TrinoRole, - rolegroup_ref: &RoleGroupRef, - trino_catalogs: &[catalog::v1alpha1::TrinoCatalog], - ) -> Result { - // Initialize the result with all default values as baseline - let conf_defaults = - v1alpha1::TrinoConfig::default_config(&self.name_any(), role, trino_catalogs); - - let role = self.role(role)?; - - // Retrieve role resource config - let mut conf_role = role.config.config.to_owned(); - - // Retrieve rolegroup specific resource config - let mut conf_rolegroup = self.rolegroup(rolegroup_ref)?.config.config.clone(); - - // Merge more specific configs into default config - // Hierarchy is: - // 1. RoleGroup - // 2. Role - // 3. Default - conf_role.merge(&conf_defaults); - conf_rolegroup.merge(&conf_role); - - tracing::debug!("Merged config: {:?}", conf_rolegroup); - fragment::validate(conf_rolegroup).context(FragmentValidationFailureSnafu) - } } +/// Converts the coordinator role (which carries the coordinator-specific `role_config`) into the +/// generic [`TrinoRoleType`]. Only the `role_config` type parameter differs between the two; the +/// `config` and `role_groups` carry over unchanged. fn extract_role_from_coordinator_config(fragment: TrinoCoordinatorRoleType) -> TrinoRoleType { Role { - config: CommonConfiguration { - config: fragment.config.config, - config_overrides: fragment.config.config_overrides, - env_overrides: fragment.config.env_overrides, - cli_overrides: fragment.config.cli_overrides, - pod_overrides: fragment.config.pod_overrides, - product_specific_common_config: fragment.config.product_specific_common_config, - }, + config: fragment.config, role_config: fragment.role_config.common, - role_groups: fragment - .role_groups - .into_iter() - .map(|(k, v)| { - ( - k, - RoleGroup { - config: CommonConfiguration { - config: v.config.config, - config_overrides: v.config.config_overrides, - env_overrides: v.config.env_overrides, - cli_overrides: v.config.cli_overrides, - pod_overrides: v.config.pod_overrides, - product_specific_common_config: v.config.product_specific_common_config, - }, - replicas: v.replicas, - }, - ) - }) - .collect(), + role_groups: fragment.role_groups, } } @@ -1118,6 +608,29 @@ mod tests { use super::*; + /// The user-provided server TLS SecretClass as `Option<&str>`, used by these CRD-defaulting + /// assertions. + fn server_secret_class(trino: &v1alpha1::TrinoCluster) -> Option<&str> { + trino + .spec + .cluster_config + .tls + .server_secret_class + .as_ref() + .map(|secret_class| secret_class.as_ref()) + } + + /// The user-provided internal TLS SecretClass as `Option<&str>`. + fn internal_secret_class(trino: &v1alpha1::TrinoCluster) -> Option<&str> { + trino + .spec + .cluster_config + .tls + .internal_secret_class + .as_ref() + .map(|secret_class| secret_class.as_ref()) + } + #[test] fn test_server_tls() { let input = r#" @@ -1133,8 +646,11 @@ mod tests { "#; let trino: v1alpha1::TrinoCluster = serde_yaml::from_str(input).expect("illegal test input"); - assert_eq!(trino.get_server_tls(), Some(TLS_DEFAULT_SECRET_CLASS)); - assert_eq!(trino.get_internal_tls(), Some(TLS_DEFAULT_SECRET_CLASS)); + assert_eq!(server_secret_class(&trino), Some(TLS_DEFAULT_SECRET_CLASS)); + assert_eq!( + internal_secret_class(&trino), + Some(TLS_DEFAULT_SECRET_CLASS) + ); let input = r#" apiVersion: trino.stackable.tech/v1alpha1 @@ -1151,8 +667,11 @@ mod tests { "#; let trino: v1alpha1::TrinoCluster = serde_yaml::from_str(input).expect("illegal test input"); - assert_eq!(trino.get_server_tls(), Some("simple-trino-server-tls")); - assert_eq!(trino.get_internal_tls(), Some(TLS_DEFAULT_SECRET_CLASS)); + assert_eq!(server_secret_class(&trino), Some("simple-trino-server-tls")); + assert_eq!( + internal_secret_class(&trino), + Some(TLS_DEFAULT_SECRET_CLASS) + ); let input = r#" apiVersion: trino.stackable.tech/v1alpha1 @@ -1170,8 +689,8 @@ mod tests { "#; let trino: v1alpha1::TrinoCluster = serde_yaml::from_str(input).expect("illegal test input"); - assert_eq!(trino.get_server_tls(), None); - assert_eq!(trino.get_internal_tls(), None); + assert_eq!(server_secret_class(&trino), None); + assert_eq!(internal_secret_class(&trino), None); let input = r#" apiVersion: trino.stackable.tech/v1alpha1 @@ -1188,8 +707,11 @@ mod tests { "#; let trino: v1alpha1::TrinoCluster = serde_yaml::from_str(input).expect("illegal test input"); - assert_eq!(trino.get_server_tls(), Some(TLS_DEFAULT_SECRET_CLASS)); - assert_eq!(trino.get_internal_tls(), Some("simple-trino-internal-tls")); + assert_eq!(server_secret_class(&trino), Some(TLS_DEFAULT_SECRET_CLASS)); + assert_eq!( + internal_secret_class(&trino), + Some("simple-trino-internal-tls") + ); } #[test] @@ -1207,8 +729,11 @@ mod tests { "#; let trino: v1alpha1::TrinoCluster = serde_yaml::from_str(input).expect("illegal test input"); - assert_eq!(trino.get_internal_tls(), Some(TLS_DEFAULT_SECRET_CLASS)); - assert_eq!(trino.get_server_tls(), Some(TLS_DEFAULT_SECRET_CLASS)); + assert_eq!( + internal_secret_class(&trino), + Some(TLS_DEFAULT_SECRET_CLASS) + ); + assert_eq!(server_secret_class(&trino), Some(TLS_DEFAULT_SECRET_CLASS)); let input = r#" apiVersion: trino.stackable.tech/v1alpha1 @@ -1225,8 +750,11 @@ mod tests { "#; let trino: v1alpha1::TrinoCluster = serde_yaml::from_str(input).expect("illegal test input"); - assert_eq!(trino.get_internal_tls(), Some("simple-trino-internal-tls")); - assert_eq!(trino.get_server_tls(), Some(TLS_DEFAULT_SECRET_CLASS)); + assert_eq!( + internal_secret_class(&trino), + Some("simple-trino-internal-tls") + ); + assert_eq!(server_secret_class(&trino), Some(TLS_DEFAULT_SECRET_CLASS)); let input = r#" apiVersion: trino.stackable.tech/v1alpha1 @@ -1244,91 +772,8 @@ mod tests { "#; let trino: v1alpha1::TrinoCluster = serde_yaml::from_str(input).expect("illegal test input"); - assert_eq!(trino.get_internal_tls(), None); - assert_eq!(trino.get_server_tls(), Some("simple-trino-server-tls")); - } - - #[test] - fn test_graceful_shutdown_timeout_default() { - let input = r#" - apiVersion: trino.stackable.tech/v1alpha1 - kind: TrinoCluster - metadata: - name: simple-trino - spec: - image: - productVersion: "479" - clusterConfig: - catalogLabelSelector: {} - "#; - let trino: v1alpha1::TrinoCluster = - serde_yaml::from_str(input).expect("illegal test input"); - assert_eq!( - trino.min_worker_graceful_shutdown_timeout(), - DEFAULT_WORKER_GRACEFUL_SHUTDOWN_TIMEOUT - ); - } - - #[test] - fn test_graceful_shutdown_timeout_on_role() { - let input = r#" - apiVersion: trino.stackable.tech/v1alpha1 - kind: TrinoCluster - metadata: - name: simple-trino - spec: - image: - productVersion: "479" - clusterConfig: - catalogLabelSelector: {} - workers: - config: - gracefulShutdownTimeout: 42h - roleGroups: - default: - replicas: 1 - "#; - let trino: v1alpha1::TrinoCluster = - serde_yaml::from_str(input).expect("illegal test input"); - assert_eq!( - trino.min_worker_graceful_shutdown_timeout(), - Duration::from_hours_unchecked(42) - ); - } - - #[test] - fn test_graceful_shutdown_timeout_on_role_and_rolegroup() { - let input = r#" - apiVersion: trino.stackable.tech/v1alpha1 - kind: TrinoCluster - metadata: - name: simple-trino - spec: - image: - productVersion: "479" - clusterConfig: - catalogLabelSelector: {} - workers: - config: - gracefulShutdownTimeout: 42h - roleGroups: - normal: - replicas: 1 - short: - replicas: 1 - config: - gracefulShutdownTimeout: 5m - long: - replicas: 1 - config: - gracefulShutdownTimeout: 7d - "#; - let trino: v1alpha1::TrinoCluster = - serde_yaml::from_str(input).expect("illegal test input"); - assert_eq!( - trino.min_worker_graceful_shutdown_timeout(), - Duration::from_minutes_unchecked(5) - ); + assert_eq!(internal_secret_class(&trino), None); + assert_eq!(server_secret_class(&trino), Some("simple-trino-server-tls")); } impl RoundtripTestData for v1alpha1::TrinoClusterSpec { diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index 223765adc..21f49cc42 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -1,5 +1,4 @@ // TODO: Look into how to properly resolve `clippy::result_large_err`. -// This will need changes in our and upstream error types. #![allow(clippy::result_large_err)] use std::sync::Arc; @@ -33,26 +32,22 @@ use stackable_operator::{ }; use crate::{ - controller::{FULL_CONTROLLER_NAME, OPERATOR_NAME}, crd::{ TrinoCluster, TrinoClusterVersion, catalog::{TrinoCatalog, TrinoCatalogVersion}, v1alpha1, }, + trino_controller::{FULL_CONTROLLER_NAME, OPERATOR_NAME}, webhooks::conversion::create_webhook_server, }; mod authentication; mod authorization; mod catalog; -mod command; mod config; mod controller; mod crd; -mod listener; -mod operations; -mod product_logging; -mod service; +mod trino_controller; mod webhooks; mod built_info { @@ -79,7 +74,7 @@ async fn main() -> anyhow::Result<()> { Command::Run(RunArguments { operator_environment, watch_namespace, - product_config, + product_config: _, maintenance, common, }) => { @@ -126,11 +121,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/trino-operator/config-spec/properties.yaml", - ])?; - let event_recorder = Arc::new(Recorder::new( client.as_kube_client(), Reporter { @@ -194,18 +184,17 @@ async fn main() -> anyhow::Result<()> { config_map_cluster_store .state() .into_iter() - .filter(move |druid| references_config_map(druid, &config_map)) - .map(|druid| ObjectRef::from_obj(&*druid)) + .filter(move |trino| references_config_map(trino, &config_map)) + .map(|trino| ObjectRef::from_obj(&*trino)) }, ) .graceful_shutdown_on(sigterm_watcher.handle()) .run( - controller::reconcile_trino, - controller::error_policy, - Arc::new(controller::Ctx { + trino_controller::reconcile_trino, + trino_controller::error_policy, + Arc::new(trino_controller::Ctx { client: client.clone(), operator_environment, - product_config, }), ) // We can let the reporting happen in the background diff --git a/rust/operator-binary/src/operations/graceful_shutdown.rs b/rust/operator-binary/src/operations/graceful_shutdown.rs deleted file mode 100644 index 50bad7ae0..000000000 --- a/rust/operator-binary/src/operations/graceful_shutdown.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! See docs/modules/trino/pages/operations/graceful-shutdown.adoc for details -//! on how the implementation works -use std::collections::BTreeMap; - -use indoc::formatdoc; -use snafu::{ResultExt, Snafu}; -use stackable_operator::{ - builder::pod::{PodBuilder, container::ContainerBuilder}, - k8s_openapi::api::core::v1::{ExecAction, LifecycleHandler}, -}; - -use crate::crd::{ - TrinoRole, WORKER_GRACEFUL_SHUTDOWN_SAFETY_OVERHEAD, WORKER_SHUTDOWN_GRACE_PERIOD, v1alpha1, -}; - -#[derive(Debug, Snafu)] -pub enum Error { - #[snafu(display("failed to set terminationGracePeriod"))] - SetTerminationGracePeriod { - source: stackable_operator::builder::pod::Error, - }, -} - -pub fn graceful_shutdown_config_properties( - trino: &v1alpha1::TrinoCluster, - role: &TrinoRole, -) -> BTreeMap> { - match role { - TrinoRole::Coordinator => { - // Only set query.max-execution-time if fault tolerant execution is not configured. - // With fault tolerant execution enabled, queries can be retried and run indefinitely. - if trino.spec.cluster_config.fault_tolerant_execution.is_none() { - let min_worker_graceful_shutdown_timeout = - trino.min_worker_graceful_shutdown_timeout(); - // We know that queries taking longer than the minimum gracefulShutdownTimeout are subject to failure. - // Read operator docs for reasoning. - BTreeMap::from([( - "query.max-execution-time".to_string(), - Some(format!( - "{}s", - min_worker_graceful_shutdown_timeout.as_secs() - )), - )]) - } else { - BTreeMap::new() - } - } - TrinoRole::Worker => BTreeMap::from([( - "shutdown.grace-period".to_string(), - Some(format!("{}s", WORKER_SHUTDOWN_GRACE_PERIOD.as_secs())), - )]), - } -} - -pub fn add_graceful_shutdown_config( - trino: &v1alpha1::TrinoCluster, - role: &TrinoRole, - merged_config: &v1alpha1::TrinoConfig, - pod_builder: &mut PodBuilder, - trino_builder: &mut ContainerBuilder, -) -> Result<(), Error> { - // This must be always set by the merge mechanism, as we provide a default value, - // users can not disable graceful shutdown. - if let Some(graceful_shutdown_timeout) = merged_config.graceful_shutdown_timeout { - match role { - TrinoRole::Coordinator => { - pod_builder - .termination_grace_period(&graceful_shutdown_timeout) - .context(SetTerminationGracePeriodSnafu)?; - } - TrinoRole::Worker => { - // We could stick `graceful_shutdown_timeout` into the Pod's `termination_grace_period_seconds` and subtract all the overheads - // from it and use that for `query.max-execution-time`. However, as `query.max-execution-time` is user-facing, we set to the configured - // `graceful_shutdown_timeout` and add the overhead to the Pod's `termination_grace_period_seconds`. - let termination_grace_period = graceful_shutdown_timeout - + 2 * WORKER_SHUTDOWN_GRACE_PERIOD - + WORKER_GRACEFUL_SHUTDOWN_SAFETY_OVERHEAD; - let termination_grace_period_seconds = termination_grace_period.as_secs(); - - pod_builder - .termination_grace_period(&termination_grace_period) - .context(SetTerminationGracePeriodSnafu)?; - trino_builder.lifecycle_pre_stop(LifecycleHandler { - exec: Some(ExecAction { - command: Some(vec![ - "/bin/bash".to_string(), - "-x".to_string(), - "-euo".to_string(), - "pipefail".to_string(), - "-c".to_string(), - // The curl does not block, but the worker process will terminate automatically once the - // graceful shutdown is complete. As the Pod gets a normal SIGTERM sent once the hook - // exited, we need to block this call for at least the same time terminationGracePeriodSeconds - // does, so that we don't kill the Pod before the terminationGracePeriodSeconds is reached. - - // FIXME: Once we have fully fledged OPA support we need to make sure that the user we choose here (e.g. admin) - // has the permissions to trigger a graceful shutdown by e.g. inserting the needed OPA rules transparently. - formatdoc!(" - curl -v --fail --insecure -X PUT -d '\"SHUTTING_DOWN\"' -H 'Content-type: application/json' -H 'X-Trino-User: graceful-shutdown-user' -H 'X-Trino-Source: Stackable data platform' {protocol}://{host}:{port}/v1/info/state >> /proc/1/fd/1 2>&1 - echo 'Successfully sent graceful shutdown command' >> /proc/1/fd/1 2>&1 - echo 'Sleeping {termination_grace_period_seconds} seconds' >> /proc/1/fd/1 2>&1 - sleep {termination_grace_period_seconds}", - protocol = trino.exposed_protocol(), - host = "127.0.0.1", - port = trino.exposed_port(), - ), - ]), - }), - ..Default::default() - }); - } - } - } - - Ok(()) -} diff --git a/rust/operator-binary/src/operations/mod.rs b/rust/operator-binary/src/operations/mod.rs deleted file mode 100644 index e1cf1fd2e..000000000 --- a/rust/operator-binary/src/operations/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod graceful_shutdown; -pub mod pdb; - -pub use graceful_shutdown::*; diff --git a/rust/operator-binary/src/operations/pdb.rs b/rust/operator-binary/src/operations/pdb.rs deleted file mode 100644 index b135cca96..000000000 --- a/rust/operator-binary/src/operations/pdb.rs +++ /dev/null @@ -1,110 +0,0 @@ -use std::cmp::max; - -use snafu::{ResultExt, Snafu}; -use stackable_operator::{ - builder::pdb::PodDisruptionBudgetBuilder, client::Client, cluster_resources::ClusterResources, - commons::pdb::PdbConfig, kube::ResourceExt, -}; - -use crate::{ - controller::{CONTROLLER_NAME, OPERATOR_NAME}, - crd::{APP_NAME, TrinoRole, 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, - trino: &v1alpha1::TrinoCluster, - role: &TrinoRole, - client: &Client, - cluster_resources: &mut ClusterResources<'_>, -) -> Result<(), Error> { - if !pdb.enabled { - return Ok(()); - } - let max_unavailable = pdb.max_unavailable.unwrap_or(match role { - TrinoRole::Coordinator => max_unavailable_coordinators(), - TrinoRole::Worker => max_unavailable_workers(trino.num_workers()), - }); - let pdb = PodDisruptionBudgetBuilder::new_with_role( - trino, - APP_NAME, - &role.to_string(), - OPERATOR_NAME, - 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_coordinators() -> u16 { - 1 -} - -fn max_unavailable_workers(num_workers: u16) -> u16 { - // As users normally scale Trino workers to achieve more performance, we can safely take out 10% of the workers. - let max_unavailable = num_workers / 10; - - // Clamp to at least a single node allowed to be offline, so we don't block Kubernetes nodes from draining. - max(max_unavailable, 1) -} - -#[cfg(test)] -mod test { - use rstest::rstest; - - use super::*; - - #[rstest] - #[case(0, 1)] - #[case(1, 1)] - #[case(2, 1)] - #[case(3, 1)] - #[case(4, 1)] - #[case(5, 1)] - #[case(6, 1)] - #[case(7, 1)] - #[case(8, 1)] - #[case(9, 1)] - #[case(10, 1)] - #[case(11, 1)] - #[case(12, 1)] - #[case(19, 1)] - #[case(20, 2)] - #[case(21, 2)] - #[case(29, 2)] - #[case(30, 3)] - #[case(31, 3)] - #[case(100, 10)] - fn test_max_unavailable_servers( - #[case] num_workers: u16, - #[case] expected_max_unavailable: u16, - ) { - let max_unavailable = max_unavailable_workers(num_workers); - assert_eq!(max_unavailable, expected_max_unavailable); - } -} diff --git a/rust/operator-binary/src/product_logging.rs b/rust/operator-binary/src/product_logging.rs deleted file mode 100644 index 0906218d0..000000000 --- a/rust/operator-binary/src/product_logging.rs +++ /dev/null @@ -1,113 +0,0 @@ -use snafu::Snafu; -use stackable_operator::{ - product_logging::{ - framework::create_vector_config, - spec::{ - AutomaticContainerLogConfig, ContainerLogConfig, ContainerLogConfigChoice, LogLevel, - Logging, - }, - }, - role_utils::RoleGroupRef, -}; -use strum::Display; - -use crate::crd::{Container, v1alpha1}; - -#[derive(Snafu, Debug)] -pub enum Error { - #[snafu(display("object has no namespace"))] - ObjectHasNoNamespace, - - #[snafu(display("failed to retrieve the ConfigMap {cm_name}"))] - ConfigMapNotFound { - source: stackable_operator::client::Error, - cm_name: String, - }, - - #[snafu(display("failed to retrieve the entry {entry} for ConfigMap {cm_name}"))] - MissingConfigMapEntry { - entry: &'static str, - cm_name: String, - }, -} - -type Result = std::result::Result; - -#[derive(Display)] -#[strum(serialize_all = "lowercase")] -pub enum TrinoLogLevel { - Debug, - Info, - Warn, - Error, - Off, -} - -impl From for TrinoLogLevel { - fn from(level: LogLevel) -> Self { - match level { - LogLevel::TRACE | LogLevel::DEBUG => Self::Debug, - LogLevel::INFO => Self::Info, - LogLevel::WARN => Self::Warn, - LogLevel::ERROR | LogLevel::FATAL => Self::Error, - LogLevel::NONE => Self::Off, - } - } -} - -/// Return the `log.properties` configuration -pub fn get_log_properties(logging: &Logging) -> Option { - if let Some(ContainerLogConfig { - choice: Some(ContainerLogConfigChoice::Automatic(log_config)), - }) = logging.containers.get(&Container::Trino) - { - Some(create_trino_log_properties(log_config)) - } else { - None - } -} - -/// Return the vector toml configuration -pub fn get_vector_toml( - rolegroup: &RoleGroupRef, - logging: &Logging, -) -> Result> { - let vector_log_config = if let Some(ContainerLogConfig { - choice: Some(ContainerLogConfigChoice::Automatic(log_config)), - }) = logging.containers.get(&Container::Vector) - { - Some(log_config) - } else { - None - }; - - if logging.enable_vector_agent { - Ok(Some(create_vector_config(rolegroup, vector_log_config))) - } else { - Ok(None) - } -} - -/// Create trino `log.properties` containing loggers and their respective log levels. -/// The operator-rs framework `LogLevel` offers more choices which are parsed to the available -/// `TrinoLogLevel`. -/// -/// The `log.properties` will adhere to the example format: -/// ``` -/// io.trino=debug -/// io.trino.server=info -/// ``` -fn create_trino_log_properties(automatic_container_config: &AutomaticContainerLogConfig) -> String { - automatic_container_config - .loggers - .iter() - .map(|(logger, config)| { - let log_level = TrinoLogLevel::from(config.level); - if logger == AutomaticContainerLogConfig::ROOT_LOGGER { - format!("={}\n", log_level) - } else { - format!("{}={}\n", logger, log_level) - } - }) - .collect::() -} diff --git a/rust/operator-binary/src/trino_controller.rs b/rust/operator-binary/src/trino_controller.rs new file mode 100644 index 000000000..eaf66aecd --- /dev/null +++ b/rust/operator-binary/src/trino_controller.rs @@ -0,0 +1,751 @@ +//! Ensures that `Pod`s are configured and running for each [`v1alpha1::TrinoCluster`] +use std::sync::Arc; + +use const_format::concatcp; +use snafu::{ResultExt, Snafu}; +use stackable_operator::{ + cli::OperatorEnvironmentOptions, + cluster_resources::ClusterResourceApplyStrategy, + commons::{random_secret_creation, rbac::build_rbac_resources}, + kube::{ + ResourceExt, + core::{DeserializeGuard, error_boundary}, + runtime::controller::Action, + }, + logging::controller::ReconcilerError, + memory::{BinaryMultiple, MemoryQuantity}, + shared::time::Duration, + status::condition::{ + compute_conditions, operations::ClusterOperationsConditionBuilder, + statefulset::StatefulSetConditionBuilder, + }, + v2::{cluster_resources::cluster_resources_new, types::operator::ClusterName}, +}; +use strum::{EnumDiscriminants, IntoStaticStr}; + +use crate::{ + controller::{ + RoleGroupName, build, + build::resource::{ + listener::{build_group_listener, group_listener_name}, + pdb::build_pdb, + service::{ + build_rolegroup_headless_service, build_rolegroup_metrics_service, + headless_service_ports, + }, + }, + controller_name, dereference, operator_name, product_name, validate, + }, + crd::{APP_NAME, ENV_INTERNAL_SECRET, ENV_SPOOLING_SECRET, v1alpha1}, +}; + +pub struct Ctx { + pub client: stackable_operator::client::Client, + pub operator_environment: OperatorEnvironmentOptions, +} + +pub const OPERATOR_NAME: &str = "trino.stackable.tech"; +pub const CONTROLLER_NAME: &str = "trinocluster"; +pub const FULL_CONTROLLER_NAME: &str = concatcp!(CONTROLLER_NAME, '.', OPERATOR_NAME); + +pub use stackable_operator::v2::product_logging::framework::STACKABLE_LOG_DIR; +pub const STACKABLE_LOG_CONFIG_DIR: &str = "/stackable/log_config"; + +pub const MAX_PREPARE_LOG_FILE_SIZE: MemoryQuantity = MemoryQuantity { + value: 1.0, + unit: BinaryMultiple::Mebi, +}; + +pub(crate) const CONTAINER_IMAGE_BASE_NAME: &str = "trino"; + +#[derive(Snafu, Debug, EnumDiscriminants)] +#[strum_discriminants(derive(IntoStaticStr))] +#[allow(clippy::enum_variant_names)] +pub enum Error { + #[snafu(display("failed to delete orphaned resources"))] + DeleteOrphanedResources { + source: stackable_operator::cluster_resources::Error, + }, + + #[snafu(display("failed to apply Service for {}", rolegroup))] + ApplyRoleGroupService { + source: stackable_operator::cluster_resources::Error, + rolegroup: RoleGroupName, + }, + + #[snafu(display("failed to build ConfigMap for {}", rolegroup))] + BuildRoleGroupConfigMap { + source: build::resource::config_map::Error, + rolegroup: RoleGroupName, + }, + + #[snafu(display("failed to apply ConfigMap for {}", rolegroup))] + ApplyRoleGroupConfig { + source: stackable_operator::cluster_resources::Error, + rolegroup: RoleGroupName, + }, + + #[snafu(display("failed to build StatefulSet for {}", rolegroup))] + BuildRoleGroupStatefulSet { + source: build::resource::statefulset::Error, + rolegroup: RoleGroupName, + }, + + #[snafu(display("failed to apply StatefulSet for {}", rolegroup))] + ApplyRoleGroupStatefulSet { + source: stackable_operator::cluster_resources::Error, + rolegroup: RoleGroupName, + }, + + #[snafu(display("failed to patch service account"))] + ApplyServiceAccount { + source: stackable_operator::cluster_resources::Error, + }, + + #[snafu(display("failed to patch role binding"))] + ApplyRoleBinding { + source: stackable_operator::cluster_resources::Error, + }, + + #[snafu(display("failed to update status"))] + ApplyStatus { + source: stackable_operator::client::Error, + }, + + #[snafu(display("failed to build RBAC resources"))] + BuildRbacResources { + source: stackable_operator::commons::rbac::Error, + }, + + #[snafu(display("failed to apply PodDisruptionBudget"))] + ApplyPdb { + source: stackable_operator::cluster_resources::Error, + }, + + #[snafu(display("failed to get required Labels"))] + GetRequiredLabels { + source: + stackable_operator::kvp::KeyValuePairError, + }, + + #[snafu(display("failed to build Labels"))] + LabelBuild { + source: stackable_operator::kvp::LabelError, + }, + + #[snafu(display("invalid TrinoCluster object"))] + InvalidTrinoCluster { + source: error_boundary::InvalidObject, + }, + + #[snafu(display("failed to dereference resources"))] + Dereference { source: dereference::Error }, + + #[snafu(display("failed to validate cluster"))] + ValidateCluster { source: validate::Error }, + + #[snafu(display("failed to apply group listener"))] + ApplyGroupListener { + source: stackable_operator::cluster_resources::Error, + }, + + #[snafu(display("failed to create internal secret"))] + CreateInternalSecret { + source: random_secret_creation::Error, + }, +} + +type Result = std::result::Result; + +impl ReconcilerError for Error { + fn category(&self) -> &'static str { + ErrorDiscriminants::from(self).into() + } +} + +pub async fn reconcile_trino( + trino: Arc>, + ctx: Arc, +) -> Result { + tracing::info!("Starting reconcile"); + + let trino = trino + .0 + .as_ref() + .map_err(error_boundary::InvalidObject::clone) + .context(InvalidTrinoClusterSnafu)?; + let client = &ctx.client; + + // dereference (client required) + let dereferenced_objects = dereference::dereference(client, trino) + .await + .context(DereferenceSnafu)?; + + // validate (no client required) + let validated_cluster = + validate::validate(trino, &dereferenced_objects, &ctx.operator_environment) + .context(ValidateClusterSnafu)?; + tracing::debug!( + trino.name = %validated_cluster.name, + trino.namespace = %validated_cluster.namespace, + trino.uid = %validated_cluster.uid, + "Validated TrinoCluster" + ); + + let mut cluster_resources = cluster_resources_new( + &product_name(), + &operator_name(), + &controller_name(), + &validated_cluster.name, + &validated_cluster.namespace, + &validated_cluster.uid, + ClusterResourceApplyStrategy::from(&trino.spec.cluster_operation), + &trino.spec.object_overrides, + ); + + let (rbac_sa, rbac_rolebinding) = build_rbac_resources( + trino, + APP_NAME, + cluster_resources + .get_required_labels() + .context(GetRequiredLabelsSnafu)?, + ) + .context(BuildRbacResourcesSnafu)?; + + let rbac_sa = cluster_resources + .add(client, rbac_sa) + .await + .context(ApplyServiceAccountSnafu)?; + + cluster_resources + .add(client, rbac_rolebinding) + .await + .context(ApplyRoleBindingSnafu)?; + + random_secret_creation::create_random_secret_if_not_exists( + &shared_internal_secret_name(&validated_cluster.name), + ENV_INTERNAL_SECRET, + 512, + &validated_cluster, + client, + ) + .await + .context(CreateInternalSecretSnafu)?; + + // This secret is created even if spooling is not configured. + // Trino currently requires the secret to be exactly 256 bits long. + random_secret_creation::create_random_secret_if_not_exists( + &shared_spooling_secret_name(&validated_cluster.name), + ENV_SPOOLING_SECRET, + 32, + &validated_cluster, + client, + ) + .await + .context(CreateInternalSecretSnafu)?; + + let mut sts_cond_builder = StatefulSetConditionBuilder::default(); + + for (trino_role, role_group_configs) in &validated_cluster.role_group_configs { + for (role_group_name, rg) in role_group_configs { + let role_group_service_recommended_labels = + validated_cluster.recommended_labels(trino_role, role_group_name); + + let role_group_service_selector = + validated_cluster.role_group_selector(trino_role, role_group_name); + + let rg_headless_service = build_rolegroup_headless_service( + &validated_cluster, + trino_role, + role_group_name, + &role_group_service_recommended_labels, + role_group_service_selector.clone().into(), + headless_service_ports(&validated_cluster), + ); + + let rg_metrics_service = build_rolegroup_metrics_service( + &validated_cluster, + trino_role, + role_group_name, + &role_group_service_recommended_labels, + role_group_service_selector.into(), + ); + + let rg_configmap = build::resource::config_map::build_rolegroup_config_map( + &validated_cluster, + trino_role, + role_group_name, + &client.kubernetes_cluster_info, + &role_group_service_recommended_labels, + ) + .with_context(|_| BuildRoleGroupConfigMapSnafu { + rolegroup: role_group_name.clone(), + })?; + + let rg_catalog_configmap = + build::resource::config_map::build_rolegroup_catalog_config_map( + &validated_cluster, + trino_role, + role_group_name, + &role_group_service_recommended_labels, + ) + .with_context(|_| BuildRoleGroupConfigMapSnafu { + rolegroup: role_group_name.clone(), + })?; + + let rg_stateful_set = build::resource::statefulset::build_rolegroup_statefulset( + &validated_cluster, + trino_role, + role_group_name, + rg, + &rbac_sa.name_any(), + ) + .with_context(|_| BuildRoleGroupStatefulSetSnafu { + rolegroup: role_group_name.clone(), + })?; + + cluster_resources + .add(client, rg_headless_service) + .await + .with_context(|_| ApplyRoleGroupServiceSnafu { + rolegroup: role_group_name.clone(), + })?; + + cluster_resources + .add(client, rg_metrics_service) + .await + .with_context(|_| ApplyRoleGroupServiceSnafu { + rolegroup: role_group_name.clone(), + })?; + + cluster_resources + .add(client, rg_configmap) + .await + .with_context(|_| ApplyRoleGroupConfigSnafu { + rolegroup: role_group_name.clone(), + })?; + + cluster_resources + .add(client, rg_catalog_configmap) + .await + .with_context(|_| ApplyRoleGroupConfigSnafu { + rolegroup: role_group_name.clone(), + })?; + + // Note: The StatefulSet needs to be applied after all ConfigMaps and Secrets it mounts + // to prevent unnecessary Pod restarts. + // See https://github.com/stackabletech/commons-operator/issues/111 for details. + sts_cond_builder.add( + cluster_resources + .add(client, rg_stateful_set) + .await + .with_context(|_| ApplyRoleGroupStatefulSetSnafu { + rolegroup: role_group_name.clone(), + })?, + ); + } + + let Some(role_config) = validated_cluster.role_config(trino_role) else { + continue; + }; + + if let Some(listener_class) = &role_config.listener_class + && let Some(listener_group_name) = group_listener_name(&validated_cluster, trino_role) + { + let role_group_listener = build_group_listener( + &validated_cluster, + validated_cluster + .recommended_labels(trino_role, &build::PLACEHOLDER_LISTENER_ROLE_GROUP), + listener_class, + listener_group_name, + ); + + cluster_resources + .add(client, role_group_listener) + .await + .context(ApplyGroupListenerSnafu)?; + } + + if let Some(pdb) = build_pdb(&role_config.pdb, &validated_cluster, trino_role) { + cluster_resources + .add(client, pdb) + .await + .context(ApplyPdbSnafu)?; + } + } + + let cluster_operation_cond_builder = + ClusterOperationsConditionBuilder::new(&trino.spec.cluster_operation); + + let status = v1alpha1::TrinoClusterStatus { + conditions: compute_conditions( + trino, + &[&sts_cond_builder, &cluster_operation_cond_builder], + ), + }; + + cluster_resources + .delete_orphaned_resources(client) + .await + .context(DeleteOrphanedResourcesSnafu)?; + client + .apply_patch_status(OPERATOR_NAME, trino, &status) + .await + .context(ApplyStatusSnafu)?; + + Ok(Action::await_change()) +} + +pub fn error_policy( + _obj: Arc>, + error: &Error, + _ctx: Arc, +) -> Action { + match error { + Error::InvalidTrinoCluster { .. } => Action::await_change(), + _ => Action::requeue(*Duration::from_secs(5)), + } +} + +pub(crate) fn shared_internal_secret_name(cluster_name: &ClusterName) -> String { + format!("{cluster_name}-internal-secret") +} + +pub(crate) fn shared_spooling_secret_name(cluster_name: &ClusterName) -> String { + format!("{cluster_name}-spooling-secret") +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use stackable_operator::{ + cli::OperatorEnvironmentOptions, commons::networking::DomainName, + k8s_openapi::api::core::v1::ConfigMap, utils::cluster_info::KubernetesClusterInfo, + v2::builder::pod::container::EnvVarName, + }; + + use super::*; + use crate::{ + authorization::opa::TrinoOpaConfig, + config::{ + client_protocol::ResolvedClientProtocolConfig, + fault_tolerant_execution::ResolvedFaultTolerantExecutionConfig, + }, + controller::dereference::DereferencedObjects, + crd::{ENV_SPOOLING_SECRET, TrinoRole, v1alpha1}, + }; + + async fn build_config_map(trino_yaml: &str) -> ConfigMap { + let deserializer = serde_yaml::Deserializer::from_str(trino_yaml); + let mut trino: v1alpha1::TrinoCluster = + serde_yaml::with::singleton_map_recursive::deserialize(deserializer) + .expect("invalid test input"); + trino.metadata.namespace = Some("default".to_owned()); + trino.metadata.uid = Some("e6ac237d-a6d4-43a1-8135-f36506110912".to_owned()); + + let cluster_info = KubernetesClusterInfo { + cluster_domain: DomainName::try_from("cluster.local").unwrap(), + }; + + let namespace = trino.metadata.namespace.clone().unwrap(); + let resolved_fte_config = match &trino.spec.cluster_config.fault_tolerant_execution { + Some(fte) => Some( + ResolvedFaultTolerantExecutionConfig::from_config(fte, None, &namespace) + .await + .unwrap(), + ), + None => None, + }; + let resolved_client_protocol_config = match &trino.spec.cluster_config.client_protocol { + Some(cp) => Some( + ResolvedClientProtocolConfig::from_config(cp, None, &namespace) + .await + .unwrap(), + ), + None => None, + }; + // Build a `TrinoOpaConfig` literal directly instead of resolving it from cluster config, + // so that `test_access_control_overrides` does not need a Kubernetes client and + // `test_config_overrides` still observes an `access-control.properties` entry in the + // rendered ConfigMap. + let trino_opa_config = Some(TrinoOpaConfig { + non_batched_connection_string: + "http://simple-opa.default.svc.cluster.local:8081/v1/data/my-product/allow" + .to_string(), + batched_connection_string: + "http://simple-opa.default.svc.cluster.local:8081/v1/data/my-product/batch" + .to_string(), + row_filters_connection_string: Some( + "http://simple-opa.default.svc.cluster.local:8081/v1/data/my-product/rowFilters" + .to_string(), + ), + batched_column_masking_connection_string: Some( + "http://simple-opa.default.svc.cluster.local:8081/v1/data/my-product/batchColumnMasks" + .to_string(), + ), + allow_permission_management_operations: true, + tls_secret_class: None, + }); + + let derefs = DereferencedObjects { + resolved_authentication_classes: Vec::new(), + catalog_definitions: Vec::new(), + catalogs: Vec::new(), + trino_opa_config, + resolved_fte_config, + resolved_client_protocol_config, + }; + + let operator_env = OperatorEnvironmentOptions { + operator_namespace: "stackable-operators".to_string(), + operator_service_name: "trino-operator".to_string(), + image_repository: "oci.example.org".to_string(), + }; + + let validated_cluster = + validate::validate(&trino, &derefs, &operator_env).expect("validate should succeed"); + + let trino_role = TrinoRole::Coordinator; + let role_group_name = RoleGroupName::from_str("default").expect("valid role group name"); + let recommended_labels = + validated_cluster.recommended_labels(&trino_role, &role_group_name); + + build::resource::config_map::build_rolegroup_config_map( + &validated_cluster, + &trino_role, + &role_group_name, + &cluster_info, + &recommended_labels, + ) + .expect("build_rolegroup_config_map should succeed") + } + + #[tokio::test] + async fn test_config_overrides() { + let trino_yaml = r#" + apiVersion: trino.stackable.tech/v1alpha1 + kind: TrinoCluster + metadata: + name: simple-trino + spec: + image: + productVersion: "479" + clusterConfig: + catalogLabelSelector: + matchLabels: + trino: simple-trino + coordinators: + configOverrides: + config.properties: + foo: bar + level: role + hello-from-role: "true" + internal-communication.https.keystore.path: /my/custom/internal-truststore.p12 + roleGroups: + default: + configOverrides: + config.properties: + foo: bar + level: role-group + hello-from-role-group: "true" + http-server.https.truststore.path: /my/custom/truststore.p12 + replicas: 1 + workers: + roleGroups: + default: + replicas: 1 + "#; + let cm = build_config_map(trino_yaml).await.data.unwrap(); + let config = cm.get("config.properties").unwrap(); + assert!(config.contains("foo=bar")); + assert!(config.contains("level=role-group")); + assert!(config.contains("hello-from-role=true")); + assert!(config.contains("hello-from-role-group=true")); + assert!(config.contains("http-server.https.enabled=true")); + assert!( + config.contains("http-server.https.keystore.path=/stackable/server_tls/keystore.p12") + ); + assert!(config.contains( + "internal-communication.https.keystore.path=/my/custom/internal-truststore.p12" + )); + // Overwritten by configOverrides from role (does work) + assert!(config.contains("http-server.https.truststore.path=/my/custom/truststore.p12")); + + assert!(cm.contains_key("jvm.config")); + assert!(cm.contains_key("security.properties")); + assert!(cm.contains_key("node.properties")); + assert!(cm.contains_key("log.properties")); + assert!(cm.contains_key("access-control.properties")); + } + + #[tokio::test] + async fn test_client_protocol_config_overrides() { + let trino_yaml = r#" + apiVersion: trino.stackable.tech/v1alpha1 + kind: TrinoCluster + metadata: + name: simple-trino + spec: + image: + productVersion: "479" + clusterConfig: + catalogLabelSelector: + matchLabels: + trino: simple-trino + clientProtocol: + spooling: + location: s3://my-bucket/spooling + filesystem: + s3: + connection: + reference: test-s3-connection + coordinators: + configOverrides: + config.properties: + foo: bar + spooling-manager.properties: + fs.location: s3a://role-level + roleGroups: + default: + replicas: 1 + configOverrides: + spooling-manager.properties: + fs.location: s3a://role-group-level + workers: + roleGroups: + default: + replicas: 1 + "#; + + let cm = build_config_map(trino_yaml).await.data.unwrap(); + let config = cm.get("config.properties").unwrap(); + assert!(config.contains("protocol.spooling.enabled=true")); + assert!(config.contains(&format!( + "protocol.spooling.shared-secret-key=${{ENV\\:{ENV_SPOOLING_SECRET}}}" + ))); + assert!(config.contains("foo=bar")); + + let config = cm.get("spooling-manager.properties").unwrap(); + assert!(config.contains("fs.location=s3a\\://role-group-level")); + assert!(config.contains("spooling-manager.name=filesystem")); + } + + #[tokio::test] + async fn test_access_control_overrides() { + let trino_yaml = r#" + apiVersion: trino.stackable.tech/v1alpha1 + kind: TrinoCluster + metadata: + name: trino + spec: + image: + productVersion: "479" + clusterConfig: + catalogLabelSelector: + matchLabels: + trino: simple-trino + authorization: + opa: + configMapName: simple-opa + package: my-product + coordinators: + configOverrides: + access-control.properties: + hello-from-role: "true" # only defined here at role level + foo.bar: "false" # overridden by role group below + opa.allow-permission-management-operations: "false" # override value from config + roleGroups: + default: + configOverrides: + access-control.properties: + hello-from-role-group: "true" # only defined here at group level + foo.bar: "true" # overrides role value + opa.policy.batched-uri: "http://simple-opa.default.svc.cluster.local:8081/v1/data/my-product/batch-new" # override value from config + opa.policy.batch-column-masking-uri: "http://simple-opa.default.svc.cluster.local:8081/v1/data/my-product/batchColumnMasks-new" # override value from config + replicas: 1 + workers: + roleGroups: + default: + replicas: 1 + "#; + + let cm = build_config_map(trino_yaml).await.data.unwrap(); + let access_control_config = cm.get("access-control.properties").unwrap(); + + assert!(access_control_config.contains("access-control.name=opa")); + assert!(access_control_config.contains("hello-from-role=true")); + assert!(access_control_config.contains("hello-from-role-group=true")); + assert!(access_control_config.contains("foo.bar=true")); + assert!(access_control_config.contains("opa.allow-permission-management-operations=false")); + assert!(access_control_config.contains(r#"opa.policy.batched-uri=http\://simple-opa.default.svc.cluster.local\:8081/v1/data/my-product/batch-new"#)); + assert!(access_control_config.contains(r#"opa.policy.batch-column-masking-uri=http\://simple-opa.default.svc.cluster.local\:8081/v1/data/my-product/batchColumnMasks-new"#)); + assert!(access_control_config.contains(r#"opa.policy.row-filters-uri=http\://simple-opa.default.svc.cluster.local\:8081/v1/data/my-product/rowFilters"#)); + assert!(access_control_config.contains(r#"opa.policy.uri=http\://simple-opa.default.svc.cluster.local\:8081/v1/data/my-product/allow"#)); + } + + #[test] + fn test_env_overrides() { + let trino_yaml = r#" + apiVersion: trino.stackable.tech/v1alpha1 + kind: TrinoCluster + metadata: + name: trino + namespace: default + uid: "e6ac237d-a6d4-43a1-8135-f36506110912" + spec: + image: + productVersion: "479" + clusterConfig: + catalogLabelSelector: + matchLabels: + trino: simple-trino + coordinators: + envOverrides: + COMMON_VAR: role-value # overridden by role group below + ROLE_VAR: role-value # only defined here at role level + roleGroups: + default: + envOverrides: + COMMON_VAR: group-value # overrides role value + GROUP_VAR: group-value # only defined here at group level + replicas: 1 + workers: + roleGroups: + default: + replicas: 1 + "#; + let deserializer = serde_yaml::Deserializer::from_str(trino_yaml); + let trino: v1alpha1::TrinoCluster = + serde_yaml::with::singleton_map_recursive::deserialize(deserializer).unwrap(); + + let derefs = DereferencedObjects { + resolved_authentication_classes: Vec::new(), + catalog_definitions: Vec::new(), + catalogs: Vec::new(), + trino_opa_config: None, + resolved_fte_config: None, + resolved_client_protocol_config: None, + }; + let operator_env = OperatorEnvironmentOptions { + operator_namespace: "stackable-operators".to_string(), + operator_service_name: "trino-operator".to_string(), + image_repository: "oci.example.org".to_string(), + }; + let validated_cluster = + validate::validate(&trino, &derefs, &operator_env).expect("validate should succeed"); + + let env = &validated_cluster.role_group_configs[&TrinoRole::Coordinator] + .values() + .next() + .unwrap() + .env_overrides; + let value = |name: &str| { + env.get(&EnvVarName::from_str_unsafe(name)) + .and_then(|env_var| env_var.value.clone()) + }; + assert_eq!(value("COMMON_VAR").as_deref(), Some("group-value")); + assert_eq!(value("GROUP_VAR").as_deref(), Some("group-value")); + assert_eq!(value("ROLE_VAR").as_deref(), Some("role-value")); + } +} diff --git a/rust/operator-binary/src/webhooks/conversion.rs b/rust/operator-binary/src/webhooks/conversion.rs index ad34f93f8..b3e1af0a5 100644 --- a/rust/operator-binary/src/webhooks/conversion.rs +++ b/rust/operator-binary/src/webhooks/conversion.rs @@ -9,10 +9,12 @@ use stackable_operator::{ }; use crate::crd::{ - FIELD_MANAGER, TrinoCluster, TrinoClusterVersion, + TrinoCluster, TrinoClusterVersion, catalog::{TrinoCatalog, TrinoCatalogVersion}, }; +const FIELD_MANAGER: &str = "trino-operator"; + /// Contains errors which can be encountered when creating the conversion webhook server and the /// CRD maintainer. #[derive(Debug, Snafu)] diff --git a/tests/templates/kuttl/smoke/14-assert.yaml.j2 b/tests/templates/kuttl/smoke/14-assert.yaml.j2 index 39b7ed1e0..6e41ce1c3 100644 --- a/tests/templates/kuttl/smoke/14-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/14-assert.yaml.j2 @@ -96,211 +96,34 @@ commands: password-authenticator.name=file {% if lookup('env', 'VECTOR_AGGREGATOR') %} vector.yaml: | - data_dir: /stackable/log/_vector-state + --- + data_dir: ${DATA_DIR} log_schema: host_key: pod sources: + # Reads the internal Vector logs vector: type: internal_logs files_stdout: type: file include: - - /stackable/log/*/*.stdout.log + - ${LOG_DIR}/*/*.stdout.log files_stderr: type: file include: - - /stackable/log/*/*.stderr.log - - files_log4j: - type: file - include: - - /stackable/log/*/*.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") - } - } - - 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 @@ -647,6 +203,7 @@ commands: .message = join!(compact([.message, stacktrace]), "\n\n") } + # Extends the processed files with the fields "container" and "file" extended_logs_files: inputs: - processed_files_* @@ -656,14 +213,21 @@ commands: if .errors == [] { del(.errors) } - . |= parse_regex!(.file, r'^/stackable/log/(?P.*?)/(?P.*?)$') + . |= parse_regex!(.file, r'^${LOG_DIR}/(?P.*?)/(?P.*?)$') + # Filters the logs of the Vector agent according to the defined log level filtered_logs_vector: inputs: - vector type: filter - condition: '!includes(["TRACE", "DEBUG"], .metadata.level)' - + condition: > + (.metadata.level == "TRACE" && "${VECTOR_FILE_LOG_LEVEL}" == "trace") || + (.metadata.level == "DEBUG" && includes(["trace", "debug"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "INFO" && includes(["trace", "debug", "info"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "WARN" && includes(["trace", "debug", "info", "warn"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "ERROR" && includes(["trace", "debug", "info", "warn", "error"], "${VECTOR_FILE_LOG_LEVEL}")) + + # Aligns the logs of the Vector agent with the common format extended_logs_vector: inputs: - filtered_logs_vector @@ -677,22 +241,24 @@ commands: del(.pid) del(.source_type) + # Add the fields "namespace", "cluster", "role" and "roleGroup" to all logs extended_logs: inputs: - extended_logs_* type: remap source: | - .namespace = "__NAMESPACE__" - .cluster = "trino" - .role = "coordinator" - .roleGroup = "default" + .namespace = "${NAMESPACE}" + .cluster = "${CLUSTER_NAME}" + .role = "${ROLE_NAME}" + .roleGroup = "${ROLE_GROUP_NAME}" sinks: + # Forward the logs to the Vector aggregator aggregator: inputs: - extended_logs type: vector - address: $VECTOR_AGGREGATOR_ADDRESS + address: ${VECTOR_AGGREGATOR_ADDRESS} {% endif %} YAMLEOF ) @@ -777,211 +343,34 @@ 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 sources: + # Reads the internal Vector logs vector: type: internal_logs files_stdout: type: file include: - - /stackable/log/*/*.stdout.log + - ${LOG_DIR}/*/*.stdout.log files_stderr: type: file include: - - /stackable/log/*/*.stderr.log - - files_log4j: - type: file - include: - - /stackable/log/*/*.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") - } - } - - 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 @@ -1328,6 +450,7 @@ commands: .message = join!(compact([.message, stacktrace]), "\n\n") } + # Extends the processed files with the fields "container" and "file" extended_logs_files: inputs: - processed_files_* @@ -1337,14 +460,21 @@ commands: if .errors == [] { del(.errors) } - . |= parse_regex!(.file, r'^/stackable/log/(?P.*?)/(?P.*?)$') + . |= parse_regex!(.file, r'^${LOG_DIR}/(?P.*?)/(?P.*?)$') + # Filters the logs of the Vector agent according to the defined log level filtered_logs_vector: inputs: - vector type: filter - condition: '!includes(["TRACE", "DEBUG"], .metadata.level)' - + condition: > + (.metadata.level == "TRACE" && "${VECTOR_FILE_LOG_LEVEL}" == "trace") || + (.metadata.level == "DEBUG" && includes(["trace", "debug"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "INFO" && includes(["trace", "debug", "info"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "WARN" && includes(["trace", "debug", "info", "warn"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "ERROR" && includes(["trace", "debug", "info", "warn", "error"], "${VECTOR_FILE_LOG_LEVEL}")) + + # Aligns the logs of the Vector agent with the common format extended_logs_vector: inputs: - filtered_logs_vector @@ -1358,22 +488,24 @@ commands: del(.pid) del(.source_type) + # Add the fields "namespace", "cluster", "role" and "roleGroup" to all logs extended_logs: inputs: - extended_logs_* type: remap source: | - .namespace = "__NAMESPACE__" - .cluster = "trino" - .role = "worker" - .roleGroup = "default" + .namespace = "${NAMESPACE}" + .cluster = "${CLUSTER_NAME}" + .role = "${ROLE_NAME}" + .roleGroup = "${ROLE_GROUP_NAME}" sinks: + # Forward the logs to the Vector aggregator aggregator: inputs: - extended_logs type: vector - address: $VECTOR_AGGREGATOR_ADDRESS + address: ${VECTOR_AGGREGATOR_ADDRESS} {% endif %} YAMLEOF )