diff --git a/.actionlint.yaml b/.actionlint.yaml new file mode 100644 index 0000000..8337548 --- /dev/null +++ b/.actionlint.yaml @@ -0,0 +1,5 @@ +--- +self-hosted-runner: + # Ubicloud machines we are using + labels: + - ubicloud-standard-8-arm diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8f0b5b1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [Unreleased] + +### Added + +- Basic operator for OpenSearch 3.x with the following configuration options ([#10]): + - Cluster operations like `reconciliationPaused` and `stopped` + - Image selection (defaults to the official OpenSearch image for now) + - Overrides (CLI, config, environment variables, Pod) + - Affinities + - Graceful shutdown timeout + - OpenSearch node roles + - Resources (CPU, memory, storage) + - PodDisruptionBudgets + - Replicas + +[#10]: https://github.com/stackabletech/opensearch-operator/pull/10 diff --git a/Cargo.lock b/Cargo.lock index 38eb291..713d17e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "ahash" @@ -62,9 +62,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -77,33 +77,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.8" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", "once_cell_polyfill", @@ -147,7 +147,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -158,7 +158,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -169,9 +169,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" @@ -229,9 +229,9 @@ dependencies = [ [[package]] name = "backon" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd0b50b1b78dbadd44ab18b3c794e496f3a139abb9fbc27d9c94c4eebbb96496" +checksum = "302eaff5357a264a2c42f127ecb8bac761cf99749fc3dc95677e2743991f99e7" dependencies = [ "fastrand", "gloo-timers", @@ -253,12 +253,6 @@ dependencies = [ "windows-targets", ] -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -307,9 +301,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytes" @@ -319,9 +313,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.24" +version = "1.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" dependencies = [ "jobserver", "libc", @@ -330,9 +324,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "chrono" @@ -349,9 +343,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.39" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" dependencies = [ "clap_builder", "clap_derive", @@ -359,9 +353,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.39" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" dependencies = [ "anstream", "anstyle", @@ -371,27 +365,27 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "concurrent-queue" @@ -521,7 +515,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -532,7 +526,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -543,7 +537,7 @@ checksum = "b9b6483c2bbed26f97861cf57651d4f2b731964a28cd2257f934a4b452480d21" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -572,7 +566,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -593,7 +587,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -631,7 +625,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -675,7 +669,7 @@ checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -724,9 +718,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "miniz_oxide", @@ -753,6 +747,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" + [[package]] name = "futures" version = "0.3.31" @@ -809,7 +809,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -830,6 +830,7 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures 0.1.31", "futures-channel", "futures-core", "futures-io", @@ -860,7 +861,7 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] @@ -914,9 +915,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" dependencies = [ "atomic-waker", "bytes", @@ -924,7 +925,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.9.0", + "indexmap 2.10.0", "slab", "tokio", "tokio-util", @@ -939,9 +940,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.15.3" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ "allocator-api2", "equivalent", @@ -950,11 +951,11 @@ dependencies = [ [[package]] name = "headers" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" dependencies = [ - "base64 0.21.7", + "base64", "bytes", "headers-core", "http", @@ -1087,9 +1088,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.6" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http", "hyper", @@ -1118,11 +1119,11 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", "futures-core", @@ -1289,12 +1290,23 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown 0.15.3", + "hashbrown 0.15.4", +] + +[[package]] +name = "io-uring" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +dependencies = [ + "bitflags", + "cfg-if", + "libc", ] [[package]] @@ -1406,7 +1418,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa60a41b57ae1a0a071af77dbcf89fc9819cfe66edaf2beeb204c34459dcf0b2" dependencies = [ - "base64 0.22.1", + "base64", "chrono", "schemars", "serde", @@ -1443,11 +1455,11 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cb276b85b6e94ded00ac8ea2c68fcf4697ea0553cb25fddc35d4a0ab718db8d" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "chrono", "either", - "futures", + "futures 0.3.31", "home", "http", "http-body", @@ -1504,7 +1516,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1518,8 +1530,8 @@ dependencies = [ "async-stream", "backon", "educe", - "futures", - "hashbrown 0.15.3", + "futures 0.3.31", + "hashbrown 0.15.4", "hostname", "json-patch", "k8s-openapi", @@ -1542,15 +1554,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.172" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libgit2-sys" -version = "0.18.1+1.9.0" +version = "0.18.2+1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1dcb20f84ffcdd825c7a311ae347cce604a6f084a767dec4a4929829645290e" +checksum = "1c42fe03df2bd3c53a3a9c7317ad91d80c81cd1fb0caec8d7cc4cd2bfa10c222" dependencies = [ "cc", "libc", @@ -1578,9 +1590,9 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -1609,9 +1621,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "mime" @@ -1621,9 +1633,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] @@ -1635,7 +1647,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -1806,9 +1818,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -1816,9 +1828,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -1833,7 +1845,7 @@ version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" dependencies = [ - "base64 0.22.1", + "base64", "serde", ] @@ -1845,9 +1857,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", "thiserror 2.0.12", @@ -1856,9 +1868,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" +checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" dependencies = [ "pest", "pest_generator", @@ -1866,24 +1878,23 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" +checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "pest_meta" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" +checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" dependencies = [ - "once_cell", "pest", "sha2", ] @@ -1905,7 +1916,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1995,7 +2006,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2009,9 +2020,9 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" @@ -2074,9 +2085,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" dependencies = [ "bitflags", ] @@ -2127,11 +2138,11 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.17" +version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f27698fc5799d2a281528a908bd874973953ca2e7bc2311637e10c263c723b" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", "futures-core", @@ -2141,11 +2152,8 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", - "ipnet", "js-sys", "log", - "mime", - "once_cell", "percent-encoding", "pin-project-lite", "serde", @@ -2178,15 +2186,15 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustls" -version = "0.23.27" +version = "0.23.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" dependencies = [ "log", "once_cell", @@ -2294,7 +2302,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2381,7 +2389,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2392,7 +2400,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2435,7 +2443,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "itoa", "ryu", "serde", @@ -2490,18 +2498,15 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "snafu" @@ -2542,7 +2547,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2567,6 +2572,8 @@ version = "0.0.0-dev" dependencies = [ "built", "clap", + "futures 0.3.31", + "schemars", "serde", "serde_json", "snafu 0.8.6", @@ -2588,8 +2595,8 @@ dependencies = [ "dockerfile-parser", "educe", "either", - "futures", - "indexmap 2.9.0", + "futures 0.3.31", + "indexmap 2.10.0", "json-patch", "k8s-openapi", "kube", @@ -2621,7 +2628,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2685,7 +2692,7 @@ dependencies = [ "kube", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2713,7 +2720,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2735,9 +2742,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.101" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -2761,7 +2768,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2790,7 +2797,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2801,17 +2808,16 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] @@ -2857,17 +2863,19 @@ dependencies = [ [[package]] name = "tokio" -version = "1.45.1" +version = "1.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "1140bb80481756a8cbe10541f37433b459c5aa1e727b4c020fbfebdc25bf3ec4" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", + "slab", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -2881,7 +2889,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2926,7 +2934,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" dependencies = [ "async-trait", - "base64 0.22.1", + "base64", "bytes", "flate2", "http", @@ -2985,11 +2993,11 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.4" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "base64 0.22.1", + "base64", "bitflags", "bytes", "futures-util", @@ -3042,20 +3050,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -3222,9 +3230,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" @@ -3257,7 +3265,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "wasm-bindgen-shared", ] @@ -3292,7 +3300,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3369,7 +3377,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -3380,14 +3388,14 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-result" @@ -3530,28 +3538,28 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -3571,7 +3579,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "synstructure", ] @@ -3611,5 +3619,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] diff --git a/Cargo.nix b/Cargo.nix index 8fe66a2..391f69c 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -120,18 +120,17 @@ rec { }; "adler2" = rec { crateName = "adler2"; - version = "2.0.0"; + version = "2.0.1"; edition = "2021"; - sha256 = "09r6drylvgy8vv8k20lnbvwq8gp09h7smfn6h1rxsy15pgh629si"; + sha256 = "1ymy18s9hs7ya1pjc9864l30wk8p2qfqdi7mhhcc5nfakxbij09j"; authors = [ "Jonas Schievink " "oyvindln " ]; features = { - "compiler_builtins" = [ "dep:compiler_builtins" ]; "core" = [ "dep:core" ]; "default" = [ "std" ]; - "rustc-dep-of-std" = [ "core" "compiler_builtins" ]; + "rustc-dep-of-std" = [ "core" ]; }; }; "ahash" = rec { @@ -253,9 +252,9 @@ rec { }; "anstream" = rec { crateName = "anstream"; - version = "0.6.18"; + version = "0.6.19"; edition = "2021"; - sha256 = "16sjk4x3ns2c3ya1x28a44kh6p47c7vhk27251i015hik1lm7k4a"; + sha256 = "0crr9a207dyn8k66xgvhvmlxm9raiwpss3syfa35c6265s9z26ih"; dependencies = [ { name = "anstyle"; @@ -298,9 +297,9 @@ rec { }; "anstyle" = rec { crateName = "anstyle"; - version = "1.0.10"; + version = "1.0.11"; edition = "2021"; - sha256 = "1yai2vppmd7zlvlrp9grwll60knrmscalf8l2qpfz8b7y5lkpk2m"; + sha256 = "1gbbzi0zbgff405q14v8hhpi1kz2drzl9a75r3qhks47lindjbl6"; features = { "default" = [ "std" ]; }; @@ -308,9 +307,9 @@ rec { }; "anstyle-parse" = rec { crateName = "anstyle-parse"; - version = "0.2.6"; + version = "0.2.7"; edition = "2021"; - sha256 = "1acqayy22fwzsrvr6n0lz6a4zvjjcvgr5sm941m7m0b2fr81cb9v"; + sha256 = "1hhmkkfr95d462b3zf6yl2vfzdqfy5726ya572wwg8ha9y148xjf"; libName = "anstyle_parse"; dependencies = [ { @@ -328,9 +327,9 @@ rec { }; "anstyle-query" = rec { crateName = "anstyle-query"; - version = "1.1.2"; + version = "1.1.3"; edition = "2021"; - sha256 = "036nm3lkyk43xbps1yql3583fp4hg3b1600is7mcyxs1gzrpm53r"; + sha256 = "1sgs2hq54wayrmpvy784ww2ccv9f8yhhpasv12z872bx0jvdx2vc"; libName = "anstyle_query"; dependencies = [ { @@ -344,9 +343,9 @@ rec { }; "anstyle-wincon" = rec { crateName = "anstyle-wincon"; - version = "3.0.8"; + version = "3.0.9"; edition = "2021"; - sha256 = "1ykkvih20kykgfix7j8y74av90m2v8ji72hv373f8vmx659dx036"; + sha256 = "10n8mcgr89risdf35i73zc67aaa392bhggwzqlri1fv79297ags0"; libName = "anstyle_wincon"; dependencies = [ { @@ -458,7 +457,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; features = [ "full" "visit-mut" ]; } ]; @@ -485,7 +484,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; usesDefaultFeatures = false; features = [ "clone-impls" "full" "parsing" "printing" "proc-macro" "visit-mut" ]; } @@ -508,9 +507,9 @@ rec { }; "autocfg" = rec { crateName = "autocfg"; - version = "1.4.0"; + version = "1.5.0"; edition = "2015"; - sha256 = "09lz3by90d2hphbq56znag9v87gfpd9gb8nr82hll8z6x2nhprdc"; + sha256 = "1s77f98id9l4af4alklmzq46f21c980v13z2r1pcxx6bqgw0d1n0"; authors = [ "Josh Stone " ]; @@ -761,9 +760,9 @@ rec { }; "backon" = rec { crateName = "backon"; - version = "1.5.0"; + version = "1.5.1"; edition = "2021"; - sha256 = "15k4p6xyxi4lkiyw5yxrmcws3wwnwjacgcqqmd2dvfldnyqm02zx"; + sha256 = "1rwr3ycl69vycyaxrhwzfjcwyqf7pawfq9zi88n4l9ks6pssybih"; dependencies = [ { name = "fastrand"; @@ -869,22 +868,7 @@ rec { }; resolvedDefaultFeatures = [ "default" "std" ]; }; - "base64 0.21.7" = rec { - crateName = "base64"; - version = "0.21.7"; - edition = "2018"; - sha256 = "0rw52yvsk75kar9wgqfwgb414kvil1gn7mqkrhn9zf1537mpsacx"; - authors = [ - "Alice Maz " - "Marshall Pierce " - ]; - features = { - "default" = [ "std" ]; - "std" = [ "alloc" ]; - }; - resolvedDefaultFeatures = [ "alloc" "default" "std" ]; - }; - "base64 0.22.1" = rec { + "base64" = rec { crateName = "base64"; version = "0.22.1"; edition = "2018"; @@ -1005,14 +989,15 @@ rec { }; "bumpalo" = rec { crateName = "bumpalo"; - version = "3.17.0"; + version = "3.19.0"; edition = "2021"; - sha256 = "1gxxsn2fsjmv03g8p3m749mczv2k4m8xspifs5l7bcx0vx3gna0n"; + sha256 = "0hsdndvcpqbjb85ghrhska2qxvp9i75q2vb70hma9fxqawdy9ia6"; authors = [ "Nick Fitzgerald " ]; features = { "allocator-api2" = [ "dep:allocator-api2" ]; + "bench_allocator_api" = [ "allocator_api" "blink-alloc/nightly" ]; "serde" = [ "dep:serde" ]; }; resolvedDefaultFeatures = [ "default" ]; @@ -1035,9 +1020,9 @@ rec { }; "cc" = rec { crateName = "cc"; - version = "1.2.24"; + version = "1.2.27"; edition = "2018"; - sha256 = "1irvbn8y9sg6f1070yg5469fxk5c3ximh24ds04kph21w0xmsn8n"; + sha256 = "1p5zfsl2mw3j46w58j2sxqkbfi49azilis5335pxlr2z3c3sm1yl"; authors = [ "Alex Crichton " ]; @@ -1067,17 +1052,16 @@ rec { }; "cfg-if" = rec { crateName = "cfg-if"; - version = "1.0.0"; + version = "1.0.1"; edition = "2018"; - sha256 = "1za0vb97n4brpzpv8lsbnzmq5r8f2b0cpqqr0sy8h5bn751xxwds"; + sha256 = "0s0jr5j797q1vqjcd41l0v5izlmlqm7lxy512b418xz5r65mfmcm"; libName = "cfg_if"; authors = [ "Alex Crichton " ]; features = { - "compiler_builtins" = [ "dep:compiler_builtins" ]; "core" = [ "dep:core" ]; - "rustc-dep-of-std" = [ "core" "compiler_builtins" ]; + "rustc-dep-of-std" = [ "core" ]; }; }; "chrono" = rec { @@ -1143,10 +1127,10 @@ rec { }; "clap" = rec { crateName = "clap"; - version = "4.5.39"; + version = "4.5.40"; edition = "2021"; crateBin = []; - sha256 = "17raqwxkhhhm80iyblp1v83fvpddkg7rgqr2cjsmz3p6kczfcq7x"; + sha256 = "03widrb9d7a0bka6lsf9r9f65zhfbkdkhm8iryycx1c63mx8idj0"; dependencies = [ { name = "clap_builder"; @@ -1185,9 +1169,9 @@ rec { }; "clap_builder" = rec { crateName = "clap_builder"; - version = "4.5.39"; + version = "4.5.40"; edition = "2021"; - sha256 = "0lggb5vscs21jliisvjjphcazzb1iw8347yp42wbwazpl6967k49"; + sha256 = "17pmcjwk6rbkizj4y5vlhrnr7b5n1ffjgh75pj66j34zrq46rip0"; dependencies = [ { name = "anstream"; @@ -1224,9 +1208,9 @@ rec { }; "clap_derive" = rec { crateName = "clap_derive"; - version = "4.5.32"; + version = "4.5.40"; edition = "2021"; - sha256 = "1mqcag8qapb5yhygg2hi153kzmbf7w5hqp3nl3fvl5cn4yp6l5q9"; + sha256 = "1kjp4928wy132inisss42750rzv0wasvbbf10w98agfcwix99iyj"; procMacro = true; dependencies = [ { @@ -1243,7 +1227,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; features = [ "full" ]; } ]; @@ -1256,16 +1240,16 @@ rec { }; "clap_lex" = rec { crateName = "clap_lex"; - version = "0.7.4"; + version = "0.7.5"; edition = "2021"; - sha256 = "19nwfls5db269js5n822vkc8dw0wjq2h1wf0hgr06ld2g52d2spl"; + sha256 = "0xb6pjza43irrl99axbhs12pxq4sr8x7xd36p703j57f5i3n2kxr"; }; "colorchoice" = rec { crateName = "colorchoice"; - version = "1.0.3"; + version = "1.0.4"; edition = "2021"; - sha256 = "1439m3r3jy3xqck8aa13q658visn71ki76qa93cy55wkmalwlqsv"; + sha256 = "0x8ymkz1xr77rcj1cfanhf416pc4v681gmkc9dzb3jqja7f62nxh"; }; "concurrent-queue" = rec { @@ -1622,7 +1606,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; features = [ "full" "extra-traits" ]; } ]; @@ -1652,7 +1636,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; } ]; @@ -1678,7 +1662,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; features = [ "full" "visit-mut" ]; } ]; @@ -1780,7 +1764,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; } ]; features = { @@ -1856,7 +1840,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; } ]; features = { @@ -1949,13 +1933,13 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; } ]; devDependencies = [ { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; features = [ "full" ]; } ]; @@ -2058,7 +2042,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; } ]; features = { @@ -2192,9 +2176,9 @@ rec { }; "flate2" = rec { crateName = "flate2"; - version = "1.1.1"; + version = "1.1.2"; edition = "2018"; - sha256 = "1kpycx57dqpkr3vp53b4nq75p9mflh0smxy8hkys4v4ndvkr5vbw"; + sha256 = "07abz7v50lkdr5fjw8zaw2v8gm2vbppc0f7nqm8x3v3gb6wpsgaa"; authors = [ "Alex Crichton " "Josh Triplett " @@ -2286,7 +2270,20 @@ rec { }; resolvedDefaultFeatures = [ "alloc" "default" "std" ]; }; - "futures" = rec { + "futures 0.1.31" = rec { + crateName = "futures"; + version = "0.1.31"; + edition = "2015"; + sha256 = "0y46qbmhi37dqkch8dlfq5aninqpzqgrr98awkb3rn4fxww1lirs"; + authors = [ + "Alex Crichton " + ]; + features = { + "default" = [ "use_std" "with-deprecated" ]; + }; + resolvedDefaultFeatures = [ "default" "use_std" "with-deprecated" ]; + }; + "futures 0.3.31" = rec { crateName = "futures"; version = "0.3.31"; edition = "2018"; @@ -2345,7 +2342,7 @@ rec { "unstable" = [ "futures-core/unstable" "futures-task/unstable" "futures-channel/unstable" "futures-io/unstable" "futures-util/unstable" ]; "write-all-vectored" = [ "futures-util/write-all-vectored" ]; }; - resolvedDefaultFeatures = [ "alloc" "async-await" "default" "executor" "futures-executor" "std" ]; + resolvedDefaultFeatures = [ "alloc" "async-await" "compat" "default" "executor" "futures-executor" "std" ]; }; "futures-channel" = rec { crateName = "futures-channel"; @@ -2448,7 +2445,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; features = [ "full" ]; } ]; @@ -2485,6 +2482,12 @@ rec { sha256 = "10aa1ar8bgkgbr4wzxlidkqkcxf77gffyj8j7768h831pcaq784z"; libName = "futures_util"; dependencies = [ + { + name = "futures"; + packageId = "futures 0.1.31"; + rename = "futures_01"; + optional = true; + } { name = "futures-channel"; packageId = "futures-channel"; @@ -2562,7 +2565,7 @@ rec { "unstable" = [ "futures-core/unstable" "futures-task/unstable" ]; "write-all-vectored" = [ "io" ]; }; - resolvedDefaultFeatures = [ "alloc" "async-await" "async-await-macro" "channel" "default" "futures-channel" "futures-io" "futures-macro" "futures-sink" "io" "memchr" "sink" "slab" "std" ]; + resolvedDefaultFeatures = [ "alloc" "async-await" "async-await-macro" "channel" "compat" "default" "futures-channel" "futures-io" "futures-macro" "futures-sink" "futures_01" "io" "memchr" "sink" "slab" "std" ]; }; "generic-array" = rec { crateName = "generic-array"; @@ -2613,7 +2616,7 @@ rec { } { name = "wasi"; - packageId = "wasi 0.11.0+wasi-snapshot-preview1"; + packageId = "wasi 0.11.1+wasi-snapshot-preview1"; usesDefaultFeatures = false; target = { target, features }: ("wasi" == target."os" or null); } @@ -2815,9 +2818,9 @@ rec { }; "h2" = rec { crateName = "h2"; - version = "0.4.10"; + version = "0.4.11"; edition = "2021"; - sha256 = "19f0va87lhzrc0lmwkgcz1z0haf6glajb4icp0b7n50vdmkilhm9"; + sha256 = "118771sqbsa6cn48y9waxq24jx80f5xy8af0lq5ixq7ifsi51nhp"; authors = [ "Carl Lerche " "Sean McArthur " @@ -2851,7 +2854,7 @@ rec { } { name = "indexmap"; - packageId = "indexmap 2.9.0"; + packageId = "indexmap 2.10.0"; features = [ "std" ]; } { @@ -2907,11 +2910,11 @@ rec { }; resolvedDefaultFeatures = [ "raw" ]; }; - "hashbrown 0.15.3" = rec { + "hashbrown 0.15.4" = rec { crateName = "hashbrown"; - version = "0.15.3"; + version = "0.15.4"; edition = "2021"; - sha256 = "1cwfw1yzkvsqkhmkg5igdvgsl8a0wyi716cn83k2j8h09ma6rcl4"; + sha256 = "1mg045sm1nm00cwjm7ndi80hcmmv1v3z7gnapxyhd9qxc62sqwar"; authors = [ "Amanieu d'Antras " ]; @@ -2939,30 +2942,29 @@ rec { features = { "alloc" = [ "dep:alloc" ]; "allocator-api2" = [ "dep:allocator-api2" ]; - "compiler_builtins" = [ "dep:compiler_builtins" ]; "core" = [ "dep:core" ]; "default" = [ "default-hasher" "inline-more" "allocator-api2" "equivalent" "raw-entry" ]; "default-hasher" = [ "dep:foldhash" ]; "equivalent" = [ "dep:equivalent" ]; "nightly" = [ "bumpalo/allocator_api" ]; "rayon" = [ "dep:rayon" ]; - "rustc-dep-of-std" = [ "nightly" "core" "compiler_builtins" "alloc" "rustc-internal-api" ]; + "rustc-dep-of-std" = [ "nightly" "core" "alloc" "rustc-internal-api" ]; "serde" = [ "dep:serde" ]; }; resolvedDefaultFeatures = [ "allocator-api2" "default" "default-hasher" "equivalent" "inline-more" "raw-entry" ]; }; "headers" = rec { crateName = "headers"; - version = "0.4.0"; - edition = "2015"; - sha256 = "1abari69kjl2yv2dg06g2x17qgd1a20xp7aqmmg2vfhcppk0c89j"; + version = "0.4.1"; + edition = "2018"; + sha256 = "1sr4zygaq1b2f0k7b5l8vx5vp05wvd82w7vpavgvr52xvdd4scdk"; authors = [ "Sean McArthur " ]; dependencies = [ { name = "base64"; - packageId = "base64 0.21.7"; + packageId = "base64"; } { name = "bytes"; @@ -3381,9 +3383,9 @@ rec { }; "hyper-rustls" = rec { crateName = "hyper-rustls"; - version = "0.27.6"; + version = "0.27.7"; edition = "2021"; - sha256 = "0va008pmz5h062wnh2h08d3r3iizvqnw68k5ji8frp0vw6aib803"; + sha256 = "0n6g8998szbzhnvcs1b7ibn745grxiqmlpg53xz206v826v3xjg3"; libName = "hyper_rustls"; dependencies = [ { @@ -3525,9 +3527,9 @@ rec { }; "hyper-util" = rec { crateName = "hyper-util"; - version = "0.1.13"; + version = "0.1.14"; edition = "2021"; - sha256 = "1s06md3mq6v6w2zqq0qfag2hw8drsvmxpiqd4mwcl7njnfv97hmi"; + sha256 = "1nqvf5azmv8p7hs5ghjlbgfya7xaafq377vppdazxbq8zzdxybyw"; libName = "hyper_util"; authors = [ "Sean McArthur " @@ -3535,7 +3537,7 @@ rec { dependencies = [ { name = "base64"; - packageId = "base64 0.22.1"; + packageId = "base64"; optional = true; } { @@ -4121,11 +4123,11 @@ rec { "serde-1" = [ "serde" ]; }; }; - "indexmap 2.9.0" = rec { + "indexmap 2.10.0" = rec { crateName = "indexmap"; - version = "2.9.0"; + version = "2.10.0"; edition = "2021"; - sha256 = "07m15a571yywmvqyb7ms717q9n42b46badbpsmx215jrg7dhv9yf"; + sha256 = "0qd6g26gxzl6dbf132w48fa8rr95glly3jhbk90i29726d9xhk7y"; dependencies = [ { name = "equivalent"; @@ -4134,7 +4136,7 @@ rec { } { name = "hashbrown"; - packageId = "hashbrown 0.15.3"; + packageId = "hashbrown 0.15.4"; usesDefaultFeatures = false; } ]; @@ -4148,6 +4150,37 @@ rec { }; resolvedDefaultFeatures = [ "default" "std" ]; }; + "io-uring" = rec { + crateName = "io-uring"; + version = "0.7.8"; + edition = "2021"; + sha256 = "04whnj5a4pml44jhsmmf4p87bpgr7swkcijx4yjcng8900pj0vmq"; + libName = "io_uring"; + authors = [ + "quininer " + ]; + dependencies = [ + { + name = "bitflags"; + packageId = "bitflags"; + } + { + name = "cfg-if"; + packageId = "cfg-if"; + } + { + name = "libc"; + packageId = "libc"; + usesDefaultFeatures = false; + } + ]; + features = { + "bindgen" = [ "dep:bindgen" ]; + "direct-syscall" = [ "sc" ]; + "overwrite" = [ "bindgen" ]; + "sc" = [ "dep:sc" ]; + }; + }; "ipnet" = rec { crateName = "ipnet"; version = "2.11.0"; @@ -4439,7 +4472,7 @@ rec { dependencies = [ { name = "base64"; - packageId = "base64 0.22.1"; + packageId = "base64"; usesDefaultFeatures = false; features = [ "alloc" ]; } @@ -4603,7 +4636,7 @@ rec { dependencies = [ { name = "base64"; - packageId = "base64 0.22.1"; + packageId = "base64"; optional = true; } { @@ -4624,7 +4657,7 @@ rec { } { name = "futures"; - packageId = "futures"; + packageId = "futures 0.3.31"; optional = true; usesDefaultFeatures = false; features = [ "std" ]; @@ -4759,7 +4792,7 @@ rec { devDependencies = [ { name = "futures"; - packageId = "futures"; + packageId = "futures 0.3.31"; usesDefaultFeatures = false; features = [ "async-await" ]; } @@ -4948,7 +4981,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; features = [ "extra-traits" ]; } ]; @@ -4997,13 +5030,13 @@ rec { } { name = "futures"; - packageId = "futures"; + packageId = "futures 0.3.31"; usesDefaultFeatures = false; features = [ "async-await" ]; } { name = "hashbrown"; - packageId = "hashbrown 0.15.3"; + packageId = "hashbrown 0.15.4"; } { name = "hostname"; @@ -5095,9 +5128,9 @@ rec { }; "libc" = rec { crateName = "libc"; - version = "0.2.172"; + version = "0.2.174"; edition = "2021"; - sha256 = "1ykz4skj7gac14znljm5clbnrhini38jkq3d60jggx3y5w2ayl6p"; + sha256 = "0xl7pqvw7g2874dy3kjady2fjr4rhj5lxsnxkkhr5689jcr6jw8i"; authors = [ "The Rust Project Developers" ]; @@ -5111,10 +5144,10 @@ rec { }; "libgit2-sys" = rec { crateName = "libgit2-sys"; - version = "0.18.1+1.9.0"; + version = "0.18.2+1.9.1"; edition = "2018"; links = "git2"; - sha256 = "03i98nb84aa99bn7sxja11pllq6fghsaw4d3qwjxikgzhh7v5p71"; + sha256 = "08n223x2pkf4gj6yrjmh3z6q236qj6nifwww78xcblrbvw1zwhhw"; libName = "libgit2_sys"; libPath = "lib.rs"; authors = [ @@ -5214,9 +5247,9 @@ rec { }; "lock_api" = rec { crateName = "lock_api"; - version = "0.4.12"; + version = "0.4.13"; edition = "2021"; - sha256 = "05qvxa6g27yyva25a5ghsg85apdxkvr77yhkyhapj6r8vnf8pbq7"; + sha256 = "0rd73p4299mjwl4hhlfj9qr88v3r0kc8s1nszkfmnq2ky43nb4wn"; authors = [ "Amanieu d'Antras " ]; @@ -5293,19 +5326,18 @@ rec { }; "memchr" = rec { crateName = "memchr"; - version = "2.7.4"; + version = "2.7.5"; edition = "2021"; - sha256 = "18z32bhxrax0fnjikv475z7ii718hq457qwmaryixfxsl2qrmjkq"; + sha256 = "1h2bh2jajkizz04fh047lpid5wgw2cr9igpkdhl3ibzscpd858ij"; authors = [ "Andrew Gallant " "bluss" ]; features = { - "compiler_builtins" = [ "dep:compiler_builtins" ]; "core" = [ "dep:core" ]; "default" = [ "std" ]; "logging" = [ "dep:log" ]; - "rustc-dep-of-std" = [ "core" "compiler_builtins" ]; + "rustc-dep-of-std" = [ "core" ]; "std" = [ "alloc" ]; "use_std" = [ "std" ]; }; @@ -5323,9 +5355,9 @@ rec { }; "miniz_oxide" = rec { crateName = "miniz_oxide"; - version = "0.8.8"; + version = "0.8.9"; edition = "2021"; - sha256 = "0al9iy33flfgxawj789w2c8xxwg1n2r5vv6m6p5hl2fvd2vlgriv"; + sha256 = "05k3pdg8bjjzayq3rf0qhpirq9k37pxnasfn4arbs17phqn6m9qz"; authors = [ "Frommi " "oyvindln " @@ -5340,10 +5372,9 @@ rec { ]; features = { "alloc" = [ "dep:alloc" ]; - "compiler_builtins" = [ "dep:compiler_builtins" ]; "core" = [ "dep:core" ]; "default" = [ "with-alloc" ]; - "rustc-dep-of-std" = [ "core" "alloc" "compiler_builtins" "adler2/rustc-dep-of-std" ]; + "rustc-dep-of-std" = [ "core" "alloc" "adler2/rustc-dep-of-std" ]; "serde" = [ "dep:serde" ]; "simd" = [ "simd-adler32" ]; "simd-adler32" = [ "dep:simd-adler32" ]; @@ -5378,7 +5409,7 @@ rec { } { name = "wasi"; - packageId = "wasi 0.11.0+wasi-snapshot-preview1"; + packageId = "wasi 0.11.1+wasi-snapshot-preview1"; target = { target, features }: ("wasi" == target."os" or null); } { @@ -6017,9 +6048,9 @@ rec { }; "parking_lot" = rec { crateName = "parking_lot"; - version = "0.12.3"; + version = "0.12.4"; edition = "2021"; - sha256 = "09ws9g6245iiq8z975h8ycf818a66q3c6zv4b5h8skpm7hc1igzi"; + sha256 = "04sab1c7304jg8k0d5b2pxbj1fvgzcf69l3n2mfpkdb96vs8pmbh"; authors = [ "Amanieu d'Antras " ]; @@ -6044,9 +6075,9 @@ rec { }; "parking_lot_core" = rec { crateName = "parking_lot_core"; - version = "0.9.10"; + version = "0.9.11"; edition = "2021"; - sha256 = "1y3cf9ld9ijf7i4igwzffcn0xl16dxyn4c5bwgjck1dkgabiyh0y"; + sha256 = "19g4d6m5k4ggacinqprnn8xvdaszc3y5smsmbz1adcdmaqm8v0xw"; authors = [ "Amanieu d'Antras " ]; @@ -6093,7 +6124,7 @@ rec { dependencies = [ { name = "base64"; - packageId = "base64 0.22.1"; + packageId = "base64"; usesDefaultFeatures = false; features = [ "alloc" ]; } @@ -6128,9 +6159,9 @@ rec { }; "pest" = rec { crateName = "pest"; - version = "2.8.0"; + version = "2.8.1"; edition = "2021"; - sha256 = "1dp741bxqiracvvwl66mfvlr29byvvph28n4c6ip136m652vg38r"; + sha256 = "08s342r6vv6ml5in4jk7pb97wgpf0frcnrvg0sqshn23sdb5zc0x"; authors = [ "Dragoș Tiselice " ]; @@ -6162,9 +6193,9 @@ rec { }; "pest_derive" = rec { crateName = "pest_derive"; - version = "2.8.0"; + version = "2.8.1"; edition = "2021"; - sha256 = "1icp5i01mgpbgwbkrcy4d0ykbxmns4wyz8j1jg6dr1wysz7xj9fp"; + sha256 = "1g20ma4y29axbjhi3z64ihhpqzmiix71qjn7bs224yd7isg6s1dv"; procMacro = true; authors = [ "Dragoș Tiselice " @@ -6191,9 +6222,9 @@ rec { }; "pest_generator" = rec { crateName = "pest_generator"; - version = "2.8.0"; + version = "2.8.1"; edition = "2021"; - sha256 = "0hgqngsxfr8y5p47bgjvd038j55ix1x4dpzr6amndaz8ddr02zfv"; + sha256 = "0rj9a20g4bjb4sl3zyzpxqg8mbn8c1kxp0nw08rfp0gp73k09r47"; authors = [ "Dragoș Tiselice " ]; @@ -6217,7 +6248,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; } ]; features = { @@ -6230,17 +6261,13 @@ rec { }; "pest_meta" = rec { crateName = "pest_meta"; - version = "2.8.0"; + version = "2.8.1"; edition = "2021"; - sha256 = "182w5fyiqm7zbn0p8313xc5wc73rnn59ycm5zk8hcja9f0j877vz"; + sha256 = "1mf01iln7shbnyxpdfnpf59gzn83nndqjkwiw3yh6n8g2wgi1lgd"; authors = [ "Dragoș Tiselice " ]; dependencies = [ - { - name = "once_cell"; - packageId = "once_cell"; - } { name = "pest"; packageId = "pest"; @@ -6290,7 +6317,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; usesDefaultFeatures = false; features = [ "parsing" "printing" "clone-impls" "proc-macro" "full" "visit-mut" ]; } @@ -6525,7 +6552,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; features = [ "extra-traits" ]; } ]; @@ -6554,15 +6581,14 @@ rec { }; "r-efi" = rec { crateName = "r-efi"; - version = "5.2.0"; + version = "5.3.0"; edition = "2018"; - sha256 = "1ig93jvpqyi87nc5kb6dri49p56q7r7qxrn8kfizmqkfj5nmyxkl"; + sha256 = "03sbfm3g7myvzyylff6qaxk4z6fy76yv860yy66jiswc2m6b7kb9"; libName = "r_efi"; features = { - "compiler_builtins" = [ "dep:compiler_builtins" ]; "core" = [ "dep:core" ]; "examples" = [ "native" ]; - "rustc-dep-of-std" = [ "compiler_builtins/rustc-dep-of-std" "core" ]; + "rustc-dep-of-std" = [ "core" ]; }; }; "rand 0.8.5" = rec { @@ -6758,9 +6784,9 @@ rec { }; "redox_syscall" = rec { crateName = "redox_syscall"; - version = "0.5.12"; + version = "0.5.13"; edition = "2021"; - sha256 = "1by5k76jr4kjy37287ifn56dzw6jh6nrwnrjm29j615ayafcm3wj"; + sha256 = "1mlzna9bcd7ss1973bmysr3hpjrys82b3bd7l03h4jkbxv8bf10d"; libName = "syscall"; authors = [ "Jeremy Soller " @@ -6956,16 +6982,16 @@ rec { }; "reqwest" = rec { crateName = "reqwest"; - version = "0.12.17"; + version = "0.12.22"; edition = "2021"; - sha256 = "0fvj7hk0rq9p2qqw4yrfr99kk5vlv25r12jjh6id56apzjc7dwn3"; + sha256 = "0cbmfrcrk6wbg93apmji0fln1ca9322af2kc7dpa18vcgs9k3jfb"; authors = [ "Sean McArthur " ]; dependencies = [ { name = "base64"; - packageId = "base64 0.22.1"; + packageId = "base64"; } { name = "bytes"; @@ -7014,11 +7040,6 @@ rec { target = { target, features }: (!("wasm32" == target."arch" or null)); features = [ "http1" "client" "client-legacy" "client-proxy" "tokio" ]; } - { - name = "ipnet"; - packageId = "ipnet"; - target = { target, features }: (!("wasm32" == target."arch" or null)); - } { name = "js-sys"; packageId = "js-sys"; @@ -7029,16 +7050,6 @@ rec { packageId = "log"; target = { target, features }: (!("wasm32" == target."arch" or null)); } - { - name = "mime"; - packageId = "mime"; - target = { target, features }: (!("wasm32" == target."arch" or null)); - } - { - name = "once_cell"; - packageId = "once_cell"; - target = { target, features }: (!("wasm32" == target."arch" or null)); - } { name = "percent-encoding"; packageId = "percent-encoding"; @@ -7171,16 +7182,16 @@ rec { "__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" = [ "dep:async-compression" "async-compression?/brotli" "dep:futures-util" "dep:tokio-util" ]; - "charset" = [ "dep:encoding_rs" ]; + "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" ]; "deflate" = [ "dep:async-compression" "async-compression?/zlib" "dep:futures-util" "dep:tokio-util" ]; "gzip" = [ "dep:async-compression" "async-compression?/gzip" "dep:futures-util" "dep:tokio-util" ]; "h2" = [ "dep:h2" ]; - "hickory-dns" = [ "dep:hickory-resolver" ]; + "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" "dep:slab" "dep:futures-channel" "tokio/macros" ]; + "http3" = [ "rustls-tls-manual-roots" "dep:h3" "dep:h3-quinn" "dep:quinn" "tokio/macros" ]; "json" = [ "dep:serde_json" ]; "macos-system-configuration" = [ "system-proxy" ]; "multipart" = [ "dep:mime_guess" "dep:futures-util" ]; @@ -7195,7 +7206,6 @@ rec { "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" ]; - "socks" = [ "dep:tokio-socks" ]; "stream" = [ "tokio/fs" "dep:futures-util" "dep:tokio-util" "dep:wasm-streams" ]; "system-proxy" = [ "hyper-util/client-proxy-system" ]; "zstd" = [ "dep:async-compression" "async-compression?/zstd" "dep:futures-util" "dep:tokio-util" ]; @@ -7265,24 +7275,23 @@ rec { }; "rustc-demangle" = rec { crateName = "rustc-demangle"; - version = "0.1.24"; + version = "0.1.25"; edition = "2015"; - sha256 = "07zysaafgrkzy2rjgwqdj2a8qdpsm6zv6f5pgpk9x0lm40z9b6vi"; + sha256 = "0kxq6m0drr40434ch32j31dkg00iaf4zxmqg7sqxajhcz0wng7lq"; libName = "rustc_demangle"; authors = [ "Alex Crichton " ]; features = { - "compiler_builtins" = [ "dep:compiler_builtins" ]; "core" = [ "dep:core" ]; - "rustc-dep-of-std" = [ "core" "compiler_builtins" ]; + "rustc-dep-of-std" = [ "core" ]; }; }; "rustls" = rec { crateName = "rustls"; - version = "0.23.27"; + version = "0.23.28"; edition = "2021"; - sha256 = "08d3nipyhmy4apksjyrb98s9k91wjyg1k7y0flx2671w135482bk"; + sha256 = "0hv6sk3r60vw11in2p8phpjd132684b4wg3zac456lzl1ghy6q3i"; dependencies = [ { name = "log"; @@ -7626,7 +7635,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; features = [ "extra-traits" ]; } ]; @@ -7880,7 +7889,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; usesDefaultFeatures = false; features = [ "clone-impls" "derive" "parsing" "printing" "proc-macro" ]; } @@ -7912,7 +7921,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; usesDefaultFeatures = false; features = [ "clone-impls" "derive" "parsing" "printing" ]; } @@ -8023,7 +8032,7 @@ rec { dependencies = [ { name = "indexmap"; - packageId = "indexmap 2.9.0"; + packageId = "indexmap 2.10.0"; } { name = "itoa"; @@ -8179,18 +8188,12 @@ rec { }; "slab" = rec { crateName = "slab"; - version = "0.4.9"; + version = "0.4.10"; edition = "2018"; - sha256 = "0rxvsgir0qw5lkycrqgb1cxsvxzjv9bmx73bk5y42svnzfba94lg"; + sha256 = "03f5a9gdp33mngya4qwq2555138pj74pl015scv57wsic5rikp04"; authors = [ "Carl Lerche " ]; - buildDependencies = [ - { - name = "autocfg"; - packageId = "autocfg"; - } - ]; features = { "default" = [ "std" ]; "serde" = [ "dep:serde" ]; @@ -8199,9 +8202,9 @@ rec { }; "smallvec" = rec { crateName = "smallvec"; - version = "1.15.0"; + version = "1.15.1"; edition = "2018"; - sha256 = "1sgfw8z729nlxk8k13dhs0a762wnaxmlx70a7xlf3wz989bjh5w9"; + sha256 = "00xxdxxpgyq5vjnpljvkmy99xij5rxgh913ii1v16kzynnivgcb7"; authors = [ "The Servo Project Developers" ]; @@ -8337,7 +8340,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; features = [ "full" ]; } ]; @@ -8405,6 +8408,15 @@ rec { name = "clap"; packageId = "clap"; } + { + name = "futures"; + packageId = "futures 0.3.31"; + features = [ "compat" ]; + } + { + name = "schemars"; + packageId = "schemars"; + } { name = "serde"; packageId = "serde"; @@ -8496,11 +8508,11 @@ rec { } { name = "futures"; - packageId = "futures"; + packageId = "futures 0.3.31"; } { name = "indexmap"; - packageId = "indexmap 2.9.0"; + packageId = "indexmap 2.10.0"; } { name = "json-patch"; @@ -8636,7 +8648,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; } ]; @@ -8899,7 +8911,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; } ]; features = { @@ -8970,7 +8982,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; features = [ "parsing" ]; } ]; @@ -9023,11 +9035,11 @@ rec { }; resolvedDefaultFeatures = [ "clone-impls" "default" "derive" "full" "parsing" "printing" "proc-macro" "quote" ]; }; - "syn 2.0.101" = rec { + "syn 2.0.104" = rec { crateName = "syn"; - version = "2.0.101"; + version = "2.0.104"; edition = "2021"; - sha256 = "1brwsh7fn3bnbj50d2lpwy9akimzb3lghz0ai89j8fhvjkybgqlc"; + sha256 = "0h2s8cxh5dsh9h41dxnlzpifqqn59cqgm0kljawws61ljq2zgdhp"; authors = [ "David Tolnay " ]; @@ -9099,7 +9111,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; usesDefaultFeatures = false; features = [ "derive" "parsing" "printing" "clone-impls" "visit" "extra-traits" ]; } @@ -9166,7 +9178,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; } ]; @@ -9192,16 +9204,16 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; } ]; }; "thread_local" = rec { crateName = "thread_local"; - version = "1.1.8"; + version = "1.1.9"; edition = "2021"; - sha256 = "173i5lyjh011gsimk21np9jn8al18rxsrkjli20a7b8ks2xgk7lb"; + sha256 = "1191jvl8d63agnq06pcnarivf63qzgpws5xa33hgc92gjjj4c0pn"; authors = [ "Amanieu d'Antras " ]; @@ -9210,10 +9222,6 @@ rec { name = "cfg-if"; packageId = "cfg-if"; } - { - name = "once_cell"; - packageId = "once_cell"; - } ]; features = { }; @@ -9367,9 +9375,9 @@ rec { }; "tokio" = rec { crateName = "tokio"; - version = "1.45.1"; + version = "1.46.0"; edition = "2021"; - sha256 = "0yb7h0mr0m0gfwdl1jir2k37gcrwhcib2kiyx9f95npi7sim3vvm"; + sha256 = "1i1ypwjxrsxz1w14qyvj3smcandl6dsg6h85w75shmhp920bnh0i"; authors = [ "Tokio Contributors " ]; @@ -9384,6 +9392,17 @@ rec { packageId = "bytes"; optional = true; } + { + name = "io-uring"; + packageId = "io-uring"; + usesDefaultFeatures = false; + target = { target, features }: ((target."tokio_uring" or false) && ("linux" == target."os" or null)); + } + { + name = "libc"; + packageId = "libc"; + target = { target, features }: ((target."tokio_uring" or false) && ("linux" == target."os" or null)); + } { name = "libc"; packageId = "libc"; @@ -9396,6 +9415,13 @@ rec { optional = true; usesDefaultFeatures = false; } + { + name = "mio"; + packageId = "mio"; + usesDefaultFeatures = false; + target = { target, features }: ((target."tokio_uring" or false) && ("linux" == target."os" or null)); + features = [ "os-poll" "os-ext" ]; + } { name = "parking_lot"; packageId = "parking_lot"; @@ -9411,6 +9437,11 @@ rec { optional = true; target = { target, features }: (target."unix" or false); } + { + name = "slab"; + packageId = "slab"; + target = { target, features }: ((target."tokio_uring" or false) && ("linux" == target."os" or null)); + } { name = "socket2"; packageId = "socket2"; @@ -9490,7 +9521,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; features = [ "full" ]; } ]; @@ -9652,7 +9683,7 @@ rec { } { name = "base64"; - packageId = "base64 0.22.1"; + packageId = "base64"; } { name = "bytes"; @@ -9998,9 +10029,9 @@ rec { }; "tower-http" = rec { crateName = "tower-http"; - version = "0.6.4"; + version = "0.6.6"; edition = "2018"; - sha256 = "0bladfcd75dkh3ikmf2m4f971nc0zn8b5pb9mdbryym27hhhrnqg"; + sha256 = "1wh51y4rf03f91c6rvli6nwzsarx7097yx6sqlm75ag27pbjzj5d"; libName = "tower_http"; authors = [ "Tower Maintainers " @@ -10008,7 +10039,7 @@ rec { dependencies = [ { name = "base64"; - packageId = "base64 0.22.1"; + packageId = "base64"; optional = true; } { @@ -10104,7 +10135,7 @@ rec { "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-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" ]; + "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" ]; "futures-core" = [ "dep:futures-core" ]; "futures-util" = [ "dep:futures-util" ]; @@ -10235,9 +10266,9 @@ rec { }; "tracing-attributes" = rec { crateName = "tracing-attributes"; - version = "0.1.28"; + version = "0.1.30"; edition = "2018"; - sha256 = "0v92l9cxs42rdm4m5hsa8z7ln1xsiw1zc2iil8c6k7lzq0jf2nir"; + sha256 = "00v9bhfgfg3v101nmmy7s3vdwadb7ngc8c1iw6wai9vj9sv3lf41"; procMacro = true; libName = "tracing_attributes"; authors = [ @@ -10256,7 +10287,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; usesDefaultFeatures = false; features = [ "full" "parsing" "printing" "visit-mut" "clone-impls" "extra-traits" "proc-macro" ]; } @@ -10266,9 +10297,9 @@ rec { }; "tracing-core" = rec { crateName = "tracing-core"; - version = "0.1.33"; + version = "0.1.34"; edition = "2018"; - sha256 = "170gc7cxyjx824r9kr17zc9gvzx89ypqfdzq259pr56gg5bwjwp6"; + sha256 = "0y3nc4mpnr79rzkrcylv5f5bnjjp19lsxwis9l4kzs97ya0jbldr"; libName = "tracing_core"; authors = [ "Tokio Contributors " @@ -10800,19 +10831,18 @@ rec { ]; }; - "wasi 0.11.0+wasi-snapshot-preview1" = rec { + "wasi 0.11.1+wasi-snapshot-preview1" = rec { crateName = "wasi"; - version = "0.11.0+wasi-snapshot-preview1"; + version = "0.11.1+wasi-snapshot-preview1"; edition = "2018"; - sha256 = "08z4hxwkpdpalxjps1ai9y7ihin26y9f476i53dv98v45gkqg3cw"; + sha256 = "0jx49r7nbkbhyfrfyhz0bm4817yrnxgd3jiwwwfv0zl439jyrwyc"; authors = [ "The Cranelift Project Developers" ]; features = { - "compiler_builtins" = [ "dep:compiler_builtins" ]; "core" = [ "dep:core" ]; "default" = [ "std" ]; - "rustc-dep-of-std" = [ "compiler_builtins" "core" "rustc-std-workspace-alloc" ]; + "rustc-dep-of-std" = [ "core" "rustc-std-workspace-alloc" ]; "rustc-std-workspace-alloc" = [ "dep:rustc-std-workspace-alloc" ]; }; resolvedDefaultFeatures = [ "default" "std" ]; @@ -10916,7 +10946,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; features = [ "full" ]; } { @@ -11017,7 +11047,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; features = [ "visit" "visit-mut" "full" ]; } { @@ -11668,7 +11698,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; usesDefaultFeatures = false; features = [ "parsing" "proc-macro" "printing" "full" "clone-impls" ]; } @@ -11698,7 +11728,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; usesDefaultFeatures = false; features = [ "parsing" "proc-macro" "printing" "full" "clone-impls" ]; } @@ -11707,9 +11737,9 @@ rec { }; "windows-link" = rec { crateName = "windows-link"; - version = "0.1.1"; + version = "0.1.3"; edition = "2021"; - sha256 = "0f2cq7imbrppsmmnz8899hfhg07cp5gq6rh0bjhb1qb6nwshk13n"; + sha256 = "12kr1p46dbhpijr4zbwr2spfgq8i8c5x55mvvfmyl96m01cx4sjy"; libName = "windows_link"; authors = [ "Microsoft" @@ -12511,7 +12541,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; features = [ "fold" ]; } { @@ -12523,9 +12553,9 @@ rec { }; "zerocopy" = rec { crateName = "zerocopy"; - version = "0.8.25"; + version = "0.8.26"; edition = "2021"; - sha256 = "1jx07cd3b3456c9al9zjqqdzpf1abb0vf6z0fj8xnb93hfajsw51"; + sha256 = "0bvsj0qzq26zc6nlrm3z10ihvjspyngs7n0jw1fz031i7h6xsf8h"; authors = [ "Joshua Liebow-Feeser " "Jack Wrenn " @@ -12559,9 +12589,9 @@ rec { }; "zerocopy-derive" = rec { crateName = "zerocopy-derive"; - version = "0.8.25"; + version = "0.8.26"; edition = "2021"; - sha256 = "1vsmpq0hp61xpqj9yk8b5jihkqkff05q1wv3l2568mhifl6y59i8"; + sha256 = "10aiywi5qkha0mpsnb1zjwi44wl2rhdncaf3ykbp4i9nqm65pkwy"; procMacro = true; libName = "zerocopy_derive"; authors = [ @@ -12579,7 +12609,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; features = [ "full" ]; } ]; @@ -12628,7 +12658,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; features = [ "fold" ]; } { @@ -12758,7 +12788,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.104"; features = [ "extra-traits" ]; } ]; diff --git a/Cargo.toml b/Cargo.toml index c9b9b02..19dce34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,8 @@ stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git", built = { version = "0.8.0", features = ["chrono", "git2"] } clap = "4.5" +futures = { version = "0.3", features = ["compat"] } +schemars = { version = "0.8.21" } # same as in operator-rs serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" snafu = "0.8" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1b9535c --- /dev/null +++ b/LICENSE @@ -0,0 +1,43 @@ +Licensed under the Open Software License version 3.0 + +1) Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + +a) to reproduce the Original Work in copies, either alone or as part of a collective work; + +b) to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + +c) to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + +d) to perform the Original Work publicly; and + +e) to display the Original Work publicly. + +2) Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + +3) Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + +4) Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + +5) External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + +6) Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + +7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + +8) Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + +9) Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + +10) Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + +11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + +12) Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + +13) Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + +14) Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +15) Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + +16) Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/deploy/config-spec/properties.yaml b/deploy/config-spec/properties.yaml new file mode 100644 index 0000000..9bd8c3b --- /dev/null +++ b/deploy/config-spec/properties.yaml @@ -0,0 +1,5 @@ +--- +version: 0.1.0 +spec: + units: [] +properties: [] diff --git a/deploy/helm/opensearch-operator/crds/crds.yaml b/deploy/helm/opensearch-operator/crds/crds.yaml index 46f9b81..6772c15 100644 --- a/deploy/helm/opensearch-operator/crds/crds.yaml +++ b/deploy/helm/opensearch-operator/crds/crds.yaml @@ -24,6 +24,435 @@ spec: properties: spec: description: A OpenSearch cluster stacklet. This resource is managed by the Stackable operator for OpenSearch. Find more information on how to use it and the resources that the operator generates in the [operator documentation](https://docs.stackable.tech/home/nightly/opensearch/). + properties: + clusterOperation: + default: + reconciliationPaused: false + stopped: false + description: '[Cluster operations](https://docs.stackable.tech/home/nightly/concepts/operations/cluster_operations) properties, allow stopping the product instance as well as pausing reconciliation.' + properties: + reconciliationPaused: + default: false + description: Flag to stop cluster reconciliation by the operator. This means that all changes in the custom resource spec are ignored until this flag is set to false or removed. The operator will however still watch the deployed resources at the time and update the custom resource status field. If applied at the same time with `stopped`, `reconciliationPaused` will take precedence over `stopped` and stop the reconciliation immediately. + type: boolean + stopped: + default: false + description: Flag to stop the cluster. This means all deployed resources (e.g. Services, StatefulSets, ConfigMaps) are kept but all deployed Pods (e.g. replicas from a StatefulSet) are scaled to 0 and therefore stopped and removed. If applied at the same time with `reconciliationPaused`, the latter will pause reconciliation and `stopped` will take no effect until `reconciliationPaused` is set to false or removed. + type: boolean + type: object + image: + anyOf: + - required: + - custom + - productVersion + - required: + - productVersion + description: |- + Specify which image to use, the easiest way is to only configure the `productVersion`. You can also configure a custom image registry to pull from, as well as completely custom images. + + Consult the [Product image selection documentation](https://docs.stackable.tech/home/nightly/concepts/product_image_selection) for details. + properties: + custom: + description: Overwrite the docker image. Specify the full docker image name, e.g. `oci.stackable.tech/sdp/superset:1.4.1-stackable2.1.0` + type: string + productVersion: + description: Version of the product, e.g. `1.4.1`. + type: string + pullPolicy: + default: Always + description: '[Pull policy](https://kubernetes.io/docs/concepts/containers/images/#image-pull-policy) used when pulling the image.' + enum: + - IfNotPresent + - Always + - Never + type: string + pullSecrets: + description: '[Image pull secrets](https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod) to pull images from a private registry.' + items: + description: LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace. + properties: + name: + description: 'Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + required: + - name + type: object + nullable: true + type: array + repo: + description: Name of the docker repo, e.g. `oci.stackable.tech/sdp` + nullable: true + type: string + stackableVersion: + description: Stackable version of the product, e.g. `23.4`, `23.4.1` or `0.0.0-dev`. If not specified, the operator will use its own version, e.g. `23.4.1`. When using a nightly operator or a pr version, it will use the nightly `0.0.0-dev` image. + nullable: true + type: string + type: object + nodes: + description: OpenSearch nodes + properties: + cliOverrides: + additionalProperties: + type: string + default: {} + type: object + config: + default: {} + properties: + affinity: + default: + nodeAffinity: null + nodeSelector: null + podAffinity: null + podAntiAffinity: null + description: These configuration settings control [Pod placement](https://docs.stackable.tech/home/nightly/concepts/operations/pod_placement). + properties: + nodeAffinity: + description: Same as the `spec.affinity.nodeAffinity` field on the Pod, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) + nullable: true + type: object + x-kubernetes-preserve-unknown-fields: true + nodeSelector: + additionalProperties: + type: string + description: Simple key-value pairs forming a nodeSelector, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) + nullable: true + type: object + podAffinity: + description: Same as the `spec.affinity.podAffinity` field on the Pod, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) + nullable: true + type: object + x-kubernetes-preserve-unknown-fields: true + podAntiAffinity: + description: Same as the `spec.affinity.podAntiAffinity` field on the Pod, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) + nullable: true + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + gracefulShutdownTimeout: + description: Time period Pods have to gracefully shut down, e.g. `30m`, `1h` or `2d`. Consult the operator documentation for details. + nullable: true + type: string + nodeRoles: + items: + enum: + - data + - ingest + - cluster_manager + - remote_cluster_client + - warm + - search + type: string + nullable: true + type: array + resources: + default: + cpu: + max: null + min: null + memory: + limit: null + runtimeLimits: {} + storage: + data: + capacity: null + description: Resource usage is configured here, this includes CPU usage, memory usage and disk storage usage, if this role needs any. + properties: + cpu: + default: + max: null + min: null + properties: + max: + description: The maximum amount of CPU cores that can be requested by Pods. Equivalent to the `limit` for Pod resource configuration. Cores are specified either as a decimal point number or as milli units. For example:`1.5` will be 1.5 cores, also written as `1500m`. + nullable: true + type: string + min: + description: The minimal amount of CPU cores that Pods need to run. Equivalent to the `request` for Pod resource configuration. Cores are specified either as a decimal point number or as milli units. For example:`1.5` will be 1.5 cores, also written as `1500m`. + nullable: true + type: string + type: object + memory: + properties: + limit: + description: 'The maximum amount of memory that should be available to the Pod. Specified as a byte [Quantity](https://kubernetes.io/docs/reference/kubernetes-api/common-definitions/quantity/), which means these suffixes are supported: E, P, T, G, M, k. You can also use the power-of-two equivalents: Ei, Pi, Ti, Gi, Mi, Ki. For example, the following represent roughly the same value: `128974848, 129e6, 129M, 128974848000m, 123Mi`' + nullable: true + type: string + runtimeLimits: + description: Additional options that can be specified. + type: object + type: object + storage: + properties: + data: + default: + capacity: null + properties: + capacity: + description: "Quantity is a fixed-point representation of a number. It provides convenient marshaling/unmarshaling in JSON and YAML, in addition to String() and AsInt64() accessors.\n\nThe serialization format is:\n\n``` ::= \n\n\t(Note that may be empty, from the \"\" case in .)\n\n ::= 0 | 1 | ... | 9 ::= | ::= | . | . | . ::= \"+\" | \"-\" ::= | ::= | | ::= Ki | Mi | Gi | Ti | Pi | Ei\n\n\t(International System of units; See: http://physics.nist.gov/cuu/Units/binary.html)\n\n ::= m | \"\" | k | M | G | T | P | E\n\n\t(Note that 1024 = 1Ki but 1000 = 1k; I didn't choose the capitalization.)\n\n ::= \"e\" | \"E\" ```\n\nNo matter which of the three exponent forms is used, no quantity may represent a number greater than 2^63-1 in magnitude, nor may it have more than 3 decimal places. Numbers larger or more precise will be capped or rounded up. (E.g.: 0.1m will rounded up to 1m.) This may be extended in the future if we require larger or smaller quantities.\n\nWhen a Quantity is parsed from a string, it will remember the type of suffix it had, and will use the same type again when it is serialized.\n\nBefore serializing, Quantity will be put in \"canonical form\". This means that Exponent/suffix will be adjusted up or down (with a corresponding increase or decrease in Mantissa) such that:\n\n- No precision is lost - No fractional digits will be emitted - The exponent (or suffix) is as large as possible.\n\nThe sign will be omitted unless the number is negative.\n\nExamples:\n\n- 1.5 will be serialized as \"1500m\" - 1.5Gi will be serialized as \"1536Mi\"\n\nNote that the quantity will NEVER be internally represented by a floating point number. That is the whole point of this exercise.\n\nNon-canonical values will still parse as long as they are well formed, but will be re-emitted in their canonical form. (So always use canonical form, or don't diff.)\n\nThis format is intended to make it difficult to use these numbers without writing some sort of special handling code in the hopes that that will cause implementors to also use a fixed point implementation." + nullable: true + type: string + selectors: + description: A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects. + nullable: true + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + storageClass: + nullable: true + type: string + type: object + type: object + type: object + type: object + configOverrides: + additionalProperties: + additionalProperties: + type: string + type: object + default: {} + description: The `configOverrides` can be used to configure properties in product config files that are not exposed in the CRD. Read the [config overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#config-overrides) and consult the operator specific usage guide documentation for details on the available config files and settings for the specific product. + type: object + envOverrides: + additionalProperties: + type: string + default: {} + description: '`envOverrides` configure environment variables to be set in the Pods. It is a map from strings to strings - environment variables and the value to set. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about the product specific environment variables that are available.' + type: object + podOverrides: + default: {} + description: In the `podOverrides` property you can define a [PodTemplateSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#podtemplatespec-v1-core) to override any property that can be set on a Kubernetes Pod. Read the [Pod overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#pod-overrides) for more information. + type: object + x-kubernetes-preserve-unknown-fields: true + roleConfig: + default: + podDisruptionBudget: + enabled: true + maxUnavailable: null + description: This is a product-agnostic RoleConfig, which is sufficient for most of the products. + properties: + podDisruptionBudget: + default: + enabled: true + maxUnavailable: null + description: |- + This struct is used to configure: + + 1. If PodDisruptionBudgets are created by the operator 2. The allowed number of Pods to be unavailable (`maxUnavailable`) + + Learn more in the [allowed Pod disruptions documentation](https://docs.stackable.tech/home/nightly/concepts/operations/pod_disruptions). + properties: + enabled: + default: true + description: Whether a PodDisruptionBudget should be written out for this role. Disabling this enables you to specify your own - custom - one. Defaults to true. + type: boolean + maxUnavailable: + description: The number of Pods that are allowed to be down because of voluntary disruptions. If you don't explicitly set this, the operator will use a sane default based upon knowledge about the individual product. + format: uint16 + minimum: 0.0 + nullable: true + type: integer + type: object + type: object + roleGroups: + additionalProperties: + properties: + cliOverrides: + additionalProperties: + type: string + default: {} + type: object + config: + default: {} + properties: + affinity: + default: + nodeAffinity: null + nodeSelector: null + podAffinity: null + podAntiAffinity: null + description: These configuration settings control [Pod placement](https://docs.stackable.tech/home/nightly/concepts/operations/pod_placement). + properties: + nodeAffinity: + description: Same as the `spec.affinity.nodeAffinity` field on the Pod, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) + nullable: true + type: object + x-kubernetes-preserve-unknown-fields: true + nodeSelector: + additionalProperties: + type: string + description: Simple key-value pairs forming a nodeSelector, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) + nullable: true + type: object + podAffinity: + description: Same as the `spec.affinity.podAffinity` field on the Pod, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) + nullable: true + type: object + x-kubernetes-preserve-unknown-fields: true + podAntiAffinity: + description: Same as the `spec.affinity.podAntiAffinity` field on the Pod, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) + nullable: true + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + gracefulShutdownTimeout: + description: Time period Pods have to gracefully shut down, e.g. `30m`, `1h` or `2d`. Consult the operator documentation for details. + nullable: true + type: string + nodeRoles: + items: + enum: + - data + - ingest + - cluster_manager + - remote_cluster_client + - warm + - search + type: string + nullable: true + type: array + resources: + default: + cpu: + max: null + min: null + memory: + limit: null + runtimeLimits: {} + storage: + data: + capacity: null + description: Resource usage is configured here, this includes CPU usage, memory usage and disk storage usage, if this role needs any. + properties: + cpu: + default: + max: null + min: null + properties: + max: + description: The maximum amount of CPU cores that can be requested by Pods. Equivalent to the `limit` for Pod resource configuration. Cores are specified either as a decimal point number or as milli units. For example:`1.5` will be 1.5 cores, also written as `1500m`. + nullable: true + type: string + min: + description: The minimal amount of CPU cores that Pods need to run. Equivalent to the `request` for Pod resource configuration. Cores are specified either as a decimal point number or as milli units. For example:`1.5` will be 1.5 cores, also written as `1500m`. + nullable: true + type: string + type: object + memory: + properties: + limit: + description: 'The maximum amount of memory that should be available to the Pod. Specified as a byte [Quantity](https://kubernetes.io/docs/reference/kubernetes-api/common-definitions/quantity/), which means these suffixes are supported: E, P, T, G, M, k. You can also use the power-of-two equivalents: Ei, Pi, Ti, Gi, Mi, Ki. For example, the following represent roughly the same value: `128974848, 129e6, 129M, 128974848000m, 123Mi`' + nullable: true + type: string + runtimeLimits: + description: Additional options that can be specified. + type: object + type: object + storage: + properties: + data: + default: + capacity: null + properties: + capacity: + description: "Quantity is a fixed-point representation of a number. It provides convenient marshaling/unmarshaling in JSON and YAML, in addition to String() and AsInt64() accessors.\n\nThe serialization format is:\n\n``` ::= \n\n\t(Note that may be empty, from the \"\" case in .)\n\n ::= 0 | 1 | ... | 9 ::= | ::= | . | . | . ::= \"+\" | \"-\" ::= | ::= | | ::= Ki | Mi | Gi | Ti | Pi | Ei\n\n\t(International System of units; See: http://physics.nist.gov/cuu/Units/binary.html)\n\n ::= m | \"\" | k | M | G | T | P | E\n\n\t(Note that 1024 = 1Ki but 1000 = 1k; I didn't choose the capitalization.)\n\n ::= \"e\" | \"E\" ```\n\nNo matter which of the three exponent forms is used, no quantity may represent a number greater than 2^63-1 in magnitude, nor may it have more than 3 decimal places. Numbers larger or more precise will be capped or rounded up. (E.g.: 0.1m will rounded up to 1m.) This may be extended in the future if we require larger or smaller quantities.\n\nWhen a Quantity is parsed from a string, it will remember the type of suffix it had, and will use the same type again when it is serialized.\n\nBefore serializing, Quantity will be put in \"canonical form\". This means that Exponent/suffix will be adjusted up or down (with a corresponding increase or decrease in Mantissa) such that:\n\n- No precision is lost - No fractional digits will be emitted - The exponent (or suffix) is as large as possible.\n\nThe sign will be omitted unless the number is negative.\n\nExamples:\n\n- 1.5 will be serialized as \"1500m\" - 1.5Gi will be serialized as \"1536Mi\"\n\nNote that the quantity will NEVER be internally represented by a floating point number. That is the whole point of this exercise.\n\nNon-canonical values will still parse as long as they are well formed, but will be re-emitted in their canonical form. (So always use canonical form, or don't diff.)\n\nThis format is intended to make it difficult to use these numbers without writing some sort of special handling code in the hopes that that will cause implementors to also use a fixed point implementation." + nullable: true + type: string + selectors: + description: A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects. + nullable: true + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + storageClass: + nullable: true + type: string + type: object + type: object + type: object + type: object + configOverrides: + additionalProperties: + additionalProperties: + type: string + type: object + default: {} + description: The `configOverrides` can be used to configure properties in product config files that are not exposed in the CRD. Read the [config overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#config-overrides) and consult the operator specific usage guide documentation for details on the available config files and settings for the specific product. + type: object + envOverrides: + additionalProperties: + type: string + default: {} + description: '`envOverrides` configure environment variables to be set in the Pods. It is a map from strings to strings - environment variables and the value to set. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about the product specific environment variables that are available.' + type: object + podOverrides: + default: {} + description: In the `podOverrides` property you can define a [PodTemplateSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#podtemplatespec-v1-core) to override any property that can be set on a Kubernetes Pod. Read the [Pod overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#pod-overrides) for more information. + type: object + x-kubernetes-preserve-unknown-fields: true + replicas: + format: uint16 + minimum: 0.0 + nullable: true + type: integer + type: object + type: object + required: + - roleGroups + type: object + required: + - image + - nodes type: object status: nullable: true diff --git a/deploy/helm/opensearch-operator/templates/roles.yaml b/deploy/helm/opensearch-operator/templates/roles.yaml new file mode 100644 index 0000000..efb1767 --- /dev/null +++ b/deploy/helm/opensearch-operator/templates/roles.yaml @@ -0,0 +1,132 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "operator.fullname" . }}-clusterrole + labels: + {{- include "operator.labels" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - nodes + verbs: + - list + - watch + - apiGroups: + - "" + resources: + - configmaps + - endpoints + - pods + - serviceaccounts + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - rbac.authorization.k8s.io + resources: + - rolebindings + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - statefulsets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - policy + resources: + - poddisruptionbudgets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get + - apiGroups: + - events.k8s.io + resources: + - events + verbs: + - create + - patch + - apiGroups: + - {{ include "operator.name" . }}.stackable.tech + resources: + - {{ include "operator.name" . }}clusters + verbs: + - get + - list + - patch + - watch + - apiGroups: + - {{ include "operator.name" . }}.stackable.tech + resources: + - {{ include "operator.name" . }}clusters/status + verbs: + - patch + - apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterroles + verbs: + - bind + resourceNames: + - {{ include "operator.name" . }}-clusterrole +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "operator.name" . }}-clusterrole + labels: + {{- include "operator.labels" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - configmaps + - secrets + - serviceaccounts + verbs: + - get + - apiGroups: + - events.k8s.io + resources: + - events + verbs: + - create + - apiGroups: + - security.openshift.io + resources: + - securitycontextconstraints + resourceNames: + - nonroot-v2 + verbs: + - use diff --git a/deploy/helm/opensearch-operator/values.yaml b/deploy/helm/opensearch-operator/values.yaml index 1d33135..c1f763f 100644 --- a/deploy/helm/opensearch-operator/values.yaml +++ b/deploy/helm/opensearch-operator/values.yaml @@ -24,15 +24,8 @@ labels: stackable.tech/vendor: Stackable podSecurityContext: {} - # fsGroup: 2000 securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 resources: limits: diff --git a/rust/operator-binary/Cargo.toml b/rust/operator-binary/Cargo.toml index f8c7c8f..9d97df9 100644 --- a/rust/operator-binary/Cargo.toml +++ b/rust/operator-binary/Cargo.toml @@ -13,6 +13,8 @@ build = "build.rs" stackable-operator.workspace = true clap.workspace = true +futures.workspace = true +schemars.workspace = true serde.workspace = true serde_json.workspace = true snafu.workspace = true diff --git a/rust/operator-binary/build.rs b/rust/operator-binary/build.rs index fa809bf..7ca1c47 100644 --- a/rust/operator-binary/build.rs +++ b/rust/operator-binary/build.rs @@ -1,3 +1,3 @@ fn main() { - built::write_built_file().unwrap(); + built::write_built_file().expect("Built file should have been written."); } diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs new file mode 100644 index 0000000..65da68f --- /dev/null +++ b/rust/operator-binary/src/controller.rs @@ -0,0 +1,280 @@ +use std::{collections::BTreeMap, marker::PhantomData, str::FromStr, sync::Arc}; + +use apply::Applier; +use build::build; +use snafu::{ResultExt, Snafu}; +use stackable_operator::{ + cluster_resources::ClusterResourceApplyStrategy, + commons::{affinity::StackableAffinity, product_image_selection::ProductImage}, + k8s_openapi::api::{ + apps::v1::StatefulSet, + core::v1::{ConfigMap, Service, ServiceAccount}, + policy::v1::PodDisruptionBudget, + rbac::v1::RoleBinding, + }, + kube::{Resource, api::ObjectMeta, core::DeserializeGuard, runtime::controller::Action}, + logging::controller::ReconcilerError, + role_utils::GenericRoleConfig, + time::Duration, +}; +use strum::{EnumDiscriminants, IntoStaticStr}; +use update_status::update_status; +use validate::validate; + +use crate::{ + crd::{ + NodeRoles, + v1alpha1::{self}, + }, + framework::{ + ClusterName, ControllerName, HasNamespace, HasObjectName, HasUid, IsLabelValue, + OperatorName, ProductName, ProductVersion, RoleGroupName, RoleName, + role_utils::{GenericProductSpecificCommonConfig, RoleGroupConfig}, + }, +}; + +mod apply; +mod build; +mod update_status; +mod validate; + +pub struct ContextNames { + pub product_name: ProductName, + pub operator_name: OperatorName, + pub controller_name: ControllerName, +} + +pub struct Context { + client: stackable_operator::client::Client, + names: ContextNames, +} + +impl Context { + pub fn new(client: stackable_operator::client::Client, operator_name: OperatorName) -> Self { + Context { + client, + names: ContextNames { + product_name: ProductName::from_str("opensearch") + .expect("should be a valid product name"), + operator_name, + controller_name: ControllerName::from_str("opensearchcluster") + .expect("should be a valid controller name"), + }, + } + } + + pub fn full_controller_name(&self) -> String { + format!( + "{}.{}", + self.names.controller_name, self.names.operator_name + ) + } +} + +#[derive(Snafu, Debug, EnumDiscriminants)] +#[strum_discriminants(derive(IntoStaticStr))] +pub enum Error { + #[snafu(display("failed to deserialize cluster definition"))] + DeserializeClusterDefinition { + // boxed because otherwise Clippy warns about a large enum variant + source: Box, + }, + + #[snafu(display("failed to validate cluster"))] + ValidateCluster { source: validate::Error }, + + #[snafu(display("failed to apply resources"))] + ApplyResources { source: apply::Error }, + + #[snafu(display("failed to update status"))] + UpdateStatus { source: update_status::Error }, +} + +type Result = std::result::Result; + +impl ReconcilerError for Error { + fn category(&self) -> &'static str { + ErrorDiscriminants::from(self).into() + } +} + +type OpenSearchRoleGroupConfig = + RoleGroupConfig; + +#[derive(Clone, Debug, PartialEq)] +pub struct ValidatedOpenSearchConfig { + pub affinity: StackableAffinity, + pub node_roles: NodeRoles, + pub resources: stackable_operator::commons::resources::Resources, + pub termination_grace_period_seconds: i64, +} + +// validated and converted to validated and safe types +// no user errors +// not restricted by CRD compliance +#[derive(Clone, Debug, PartialEq)] +pub struct ValidatedCluster { + metadata: ObjectMeta, + pub image: ProductImage, + pub product_version: ProductVersion, + pub name: ClusterName, + pub namespace: String, + pub uid: String, + pub role_config: GenericRoleConfig, + // "validated" means that labels are valid and no ugly rolegroup name broke them + pub role_group_configs: BTreeMap, +} + +impl ValidatedCluster { + pub fn role_name() -> RoleName { + RoleName::from_str("nodes").expect("should be a valid role name") + } + + pub fn is_single_node(&self) -> bool { + self.node_count() == 1 + } + + pub fn node_count(&self) -> u32 { + self.role_group_configs + .values() + .map(|rg| rg.replicas as u32) + .sum() + } + + pub fn role_group_configs_filtered_by_node_role( + &self, + node_role: &v1alpha1::NodeRole, + ) -> BTreeMap { + self.role_group_configs + .clone() + .into_iter() + .filter(|c| c.1.config.node_roles.contains(node_role)) + .collect() + } +} + +impl HasObjectName for ValidatedCluster { + fn to_object_name(&self) -> String { + self.name.to_object_name() + } +} + +impl HasNamespace for ValidatedCluster { + fn to_namespace(&self) -> String { + self.namespace.clone() + } +} + +impl HasUid for ValidatedCluster { + fn to_uid(&self) -> String { + self.uid.clone() + } +} + +// ? +impl IsLabelValue for ValidatedCluster { + fn to_label_value(&self) -> String { + // opinionated! + self.name.to_label_value() + } +} + +// TODO Remove boilerplate (like derive_more) +impl Resource for ValidatedCluster { + type DynamicType = + ::DynamicType; + type Scope = ::Scope; + + fn kind(dt: &Self::DynamicType) -> std::borrow::Cow<'_, str> { + v1alpha1::OpenSearchCluster::kind(dt) + } + + fn group(dt: &Self::DynamicType) -> std::borrow::Cow<'_, str> { + v1alpha1::OpenSearchCluster::group(dt) + } + + fn version(dt: &Self::DynamicType) -> std::borrow::Cow<'_, str> { + v1alpha1::OpenSearchCluster::version(dt) + } + + fn plural(dt: &Self::DynamicType) -> std::borrow::Cow<'_, str> { + v1alpha1::OpenSearchCluster::plural(dt) + } + + fn meta(&self) -> &stackable_operator::kube::api::ObjectMeta { + &self.metadata + } + + fn meta_mut(&mut self) -> &mut stackable_operator::kube::api::ObjectMeta { + &mut self.metadata + } +} + +pub fn error_policy( + _object: Arc>, + error: &Error, + _context: Arc, +) -> Action { + match error { + // root object is invalid, will be requed when modified + Error::DeserializeClusterDefinition { .. } => Action::await_change(), + _ => Action::requeue(*Duration::from_secs(5)), + } +} + +pub async fn reconcile( + object: Arc>, + context: Arc, +) -> Result { + tracing::info!("Starting reconcile"); + + let cluster = object + .0 + .as_ref() + .map_err(stackable_operator::kube::core::error_boundary::InvalidObject::clone) + .map_err(Box::new) + .context(DeserializeClusterDefinitionSnafu)?; + + // dereference (client required) + + // validate (no client required) + let validated_cluster = validate(&context.names, cluster).context(ValidateClusterSnafu)?; + + // build (no client required; infallible) + let prepared_resources = build(&context.names, validated_cluster.clone()); + + // apply (client required) + let apply_strategy = ClusterResourceApplyStrategy::from(&cluster.spec.cluster_operation); + let applied_resources = Applier::new( + &context.client, + &context.names, + &validated_cluster, + apply_strategy, + ) + .apply(prepared_resources) + .await + .context(ApplyResourcesSnafu)?; + + // create discovery ConfigMap based on the applied resources (client required) + + // update status (client required) + update_status(&context.client, &context.names, cluster, applied_resources) + .await + .context(UpdateStatusSnafu)?; + + Ok(Action::await_change()) +} + +// Marker +struct Prepared; +struct Applied; + +struct KubernetesResources { + stateful_sets: Vec, + services: Vec, + config_maps: Vec, + service_accounts: Vec, + role_bindings: Vec, + pod_disruption_budgets: Vec, + status: PhantomData, +} diff --git a/rust/operator-binary/src/controller/apply.rs b/rust/operator-binary/src/controller/apply.rs new file mode 100644 index 0000000..23b6f2e --- /dev/null +++ b/rust/operator-binary/src/controller/apply.rs @@ -0,0 +1,106 @@ +use std::marker::PhantomData; + +use snafu::{ResultExt, Snafu}; +use stackable_operator::{ + client::Client, + cluster_resources::{ClusterResource, ClusterResourceApplyStrategy, ClusterResources}, +}; +use strum::{EnumDiscriminants, IntoStaticStr}; + +use super::{Applied, ContextNames, KubernetesResources, Prepared}; +use crate::framework::{ + HasNamespace, HasObjectName, HasUid, cluster_resources::cluster_resources_new, +}; + +#[derive(Snafu, Debug, EnumDiscriminants)] +#[strum_discriminants(derive(IntoStaticStr))] +pub enum Error { + #[snafu(display("failed to apply resource"))] + ApplyResource { + source: stackable_operator::cluster_resources::Error, + }, + + #[snafu(display("failed to delete orphaned resources"))] + DeleteOrphanedResources { + source: stackable_operator::cluster_resources::Error, + }, +} + +type Result = std::result::Result; + +pub struct Applier<'a> { + client: &'a Client, + cluster_resources: ClusterResources, +} + +impl<'a> Applier<'a> { + pub fn new( + client: &'a Client, + names: &ContextNames, + cluster: &(impl HasObjectName + HasNamespace + HasUid), + apply_strategy: ClusterResourceApplyStrategy, + ) -> Applier<'a> { + let cluster_resources = cluster_resources_new( + &names.product_name, + &names.operator_name, + &names.controller_name, + cluster, + apply_strategy, + ); + + Applier { + client, + cluster_resources, + } + } + + pub async fn apply( + mut self, + resources: KubernetesResources, + ) -> Result> { + let stateful_sets = self.add_resources(resources.stateful_sets).await?; + + let services = self.add_resources(resources.services).await?; + + let config_maps = self.add_resources(resources.config_maps).await?; + + let service_accounts = self.add_resources(resources.service_accounts).await?; + + let role_bindings = self.add_resources(resources.role_bindings).await?; + + let pod_disruption_budgets = self.add_resources(resources.pod_disruption_budgets).await?; + + self.cluster_resources + .delete_orphaned_resources(self.client) + .await + .context(DeleteOrphanedResourcesSnafu)?; + + Ok(KubernetesResources { + stateful_sets, + services, + config_maps, + service_accounts, + role_bindings, + pod_disruption_budgets, + status: PhantomData, + }) + } + + async fn add_resources( + &mut self, + resources: Vec, + ) -> Result> { + let mut applied_resources = vec![]; + + for resource in resources { + let applied_resource = self + .cluster_resources + .add(self.client, resource) + .await + .context(ApplyResourceSnafu)?; + applied_resources.push(applied_resource); + } + + Ok(applied_resources) + } +} diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs new file mode 100644 index 0000000..28aa9ad --- /dev/null +++ b/rust/operator-binary/src/controller/build.rs @@ -0,0 +1,42 @@ +use std::marker::PhantomData; + +use role_builder::RoleBuilder; + +use super::{ContextNames, KubernetesResources, Prepared, ValidatedCluster}; + +pub mod node_config; +pub mod role_builder; +pub mod role_group_builder; + +pub fn build(names: &ContextNames, cluster: ValidatedCluster) -> KubernetesResources { + let mut config_maps = vec![]; + let mut stateful_sets = vec![]; + let mut services = vec![]; + + let role_builder = RoleBuilder::new(cluster.clone(), names); + + for role_group_builder in role_builder.role_group_builders() { + config_maps.push(role_group_builder.build_config_map()); + stateful_sets.push(role_group_builder.build_stateful_set()); + services.push(role_group_builder.build_headless_service()); + } + + let cluster_manager_service = role_builder.build_cluster_manager_service(); + services.push(cluster_manager_service); + + let service_accounts = vec![role_builder.build_service_account()]; + + let role_bindings = vec![role_builder.build_role_binding()]; + + let pod_disruption_budgets = role_builder.build_pdb().into_iter().collect(); + + KubernetesResources { + stateful_sets, + services, + config_maps, + service_accounts, + role_bindings, + pod_disruption_budgets, + status: PhantomData, + } +} diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs new file mode 100644 index 0000000..49da561 --- /dev/null +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -0,0 +1,345 @@ +use serde_json::{Value, json}; +use stackable_operator::builder::pod::container::FieldPathEnvVar; + +use super::ValidatedCluster; +use crate::{ + controller::OpenSearchRoleGroupConfig, + crd::v1alpha1, + framework::{builder::pod::container::EnvVarSet, role_group_utils}, +}; + +pub const CONFIGURATION_FILE_OPENSEARCH_YML: &str = "opensearch.yml"; + +// TODO Document how to enter config_overrides of various types, e.g. string, list, boolean, +// object, ... + +// Configuration file format +// +// This is not well documented. +// +// A list setting can be written as +// - a comma-separated list, e.g. +// ``` +// setting: a,b,c +// ``` +// Commas in the values cannot be escaped. +// - a JSON list, e.g. +// ``` +// setting: ["a", "b", "c"] +// ``` +// - a YAML list, e.g. +// ``` +// setting: +// - a +// - b +// - c +// ``` +// - a (legacy) flat list, e.g. +// ``` +// setting.0: a +// setting.1: b +// setting.2: b +// ``` + +/// type: string +pub const CONFIG_OPTION_CLUSTER_NAME: &str = "cluster.name"; + +/// type: list of strings +pub const CONFIG_OPTION_DISCOVERY_SEED_HOSTS: &str = "discovery.seed_hosts"; + +/// type: string +pub const CONFIG_OPTION_DISCOVERY_TYPE: &str = "discovery.type"; + +/// type: list of strings +pub const CONFIG_OPTION_INITIAL_CLUSTER_MANAGER_NODES: &str = + "cluster.initial_cluster_manager_nodes"; + +/// type: string +pub const CONFIG_OPTION_NETWORK_HOST: &str = "network.host"; + +/// type: string +pub const CONFIG_OPTION_NODE_NAME: &str = "node.name"; + +/// type: list of strings +pub const CONFIG_OPTION_NODE_ROLES: &str = "node.roles"; + +/// type: list of strings +pub const CONFIG_OPTION_PLUGINS_SECURITY_NODES_DN: &str = "plugins.security.nodes_dn"; + +pub struct NodeConfig { + cluster: ValidatedCluster, + role_group_config: OpenSearchRoleGroupConfig, + discovery_service_name: String, +} + +// Most functions are public because their configuration values could also be used in environment +// variables. +impl NodeConfig { + pub fn new( + cluster: ValidatedCluster, + role_group_config: OpenSearchRoleGroupConfig, + discovery_service_name: String, + ) -> Self { + Self { + cluster, + role_group_config, + discovery_service_name, + } + } + + /// static for the cluster + pub fn static_opensearch_config(&self) -> String { + let mut config = serde_json::Map::new(); + + config.insert( + CONFIG_OPTION_CLUSTER_NAME.to_owned(), + json!(self.cluster.name.to_string()), + ); + config.insert( + CONFIG_OPTION_NETWORK_HOST.to_owned(), + // Bind to all interfaces because the IP address is not known in advance. + json!("0.0.0.0".to_owned()), + ); + config.insert( + CONFIG_OPTION_DISCOVERY_TYPE.to_owned(), + json!(self.discovery_type()), + ); + config.insert + // Accept certificates generated by the secret-operator + ( + CONFIG_OPTION_PLUGINS_SECURITY_NODES_DN.to_owned(), + json!(["CN=generated certificate for pod".to_owned()]), + ); + + for (setting, value) in self + .role_group_config + .config_overrides + .get(CONFIGURATION_FILE_OPENSEARCH_YML) + .into_iter() + .flatten() + { + config.insert(setting.to_owned(), json!(value)); + } + + // Ensure a deterministic result + config.sort_keys(); + + Self::to_yaml(config) + } + + /// different for every node + pub fn environment_variables(&self) -> EnvVarSet { + EnvVarSet::new() + // Set the OpenSearch node name to the Pod name. + // The node name is used e.g. for `{INITIAL_CLUSTER_MANAGER_NODES}`. + .with_field_path(CONFIG_OPTION_NODE_NAME, FieldPathEnvVar::Name) + .with_value( + CONFIG_OPTION_DISCOVERY_SEED_HOSTS, + &self.discovery_service_name, + ) + .with_value( + CONFIG_OPTION_INITIAL_CLUSTER_MANAGER_NODES, + self.initial_cluster_manager_nodes(), + ) + .with_value( + CONFIG_OPTION_NODE_ROLES, + self.role_group_config + .config + .node_roles + .iter() + .map(|node_role| format!("{node_role}")) + .collect::>() + // Node roles cannot contain commas, therefore creating a comma-separated list + // is safe. + .join(","), + ) + .with_values(self.role_group_config.env_overrides.clone()) + } + + fn to_yaml(kv: serde_json::Map) -> String { + kv.iter() + .map(|(key, value)| format!("{key}: {value}")) + .collect::>() + .join("\n") + } + + /// Configuration for `{DISCOVERY_TYPE}` + /// + /// "zen" is the default if `{DISCOVERY_TYPE}` is not set. + /// It is nevertheless explicitly set here. + /// see + /// + /// "single-node" disables the bootstrap checks, like validating the JVM and discovery + /// configurations. + pub fn discovery_type(&self) -> String { + if self.cluster.is_single_node() { + "single-node".to_owned() + } else { + "zen".to_owned() + } + } + + /// Configuration for `cluster.initial_cluster_manager_nodes` which replaces + /// `cluster.initial_master_nodes`, see + /// . + /// + /// According to + /// , + /// it contains "a list of cluster-manager-eligible nodes used to bootstrap the cluster." + /// + /// However, the documentation for Elasticsearch is more detailed and contains the following + /// notes (see ): + /// * Remove this setting once the cluster has formed, and never set it again for this cluster. + /// * Do not configure this setting on master-ineligible nodes. + /// * Do not configure this setting on nodes joining an existing cluster. + /// * Do not configure this setting on nodes which are restarting. + /// * Do not configure this setting when performing a full-cluster restart. + /// + /// The OpenSearch Helm chart only sets master nodes but does not handle the other cases (see + /// ), + /// so they are also ignored here for the moment. + fn initial_cluster_manager_nodes(&self) -> String { + if !self.cluster.is_single_node() + && self + .role_group_config + .config + .node_roles + .contains(&v1alpha1::NodeRole::ClusterManager) + { + let cluster_manager_configs = self + .cluster + .role_group_configs_filtered_by_node_role(&v1alpha1::NodeRole::ClusterManager); + + // This setting requires node names as set in `{NODE_NAME}`. + // The node names are set to the pod names with + // `valueFrom.fieldRef.fieldPath: metadata.name`, so it is okay to calculate the pod + // names here and use them as node names. + let mut pod_names = vec![]; + for (role_group_name, role_group_config) in cluster_manager_configs { + let role_group_resource_names = role_group_utils::ResourceNames { + cluster_name: self.cluster.name.clone(), + role_name: ValidatedCluster::role_name(), + role_group_name, + }; + + pod_names.extend( + (0..role_group_config.replicas) + .map(|i| format!("{}-{i}", role_group_resource_names.stateful_set_name())), + ); + } + // Pod names cannot contain commas, therefore creating a comma-separated list is safe. + pod_names.join(",") + } else { + // This setting is not allowed on single node cluster, see + // + String::new() + } + } +} + +#[cfg(test)] +mod tests { + + use std::{ + collections::{BTreeMap, HashMap}, + str::FromStr, + }; + + use stackable_operator::{ + commons::{ + affinity::StackableAffinity, product_image_selection::ProductImage, + resources::Resources, + }, + k8s_openapi::api::core::v1::{EnvVar, EnvVarSource, ObjectFieldSelector, PodTemplateSpec}, + kube::api::ObjectMeta, + role_utils::GenericRoleConfig, + }; + + use super::*; + use crate::{ + controller::ValidatedOpenSearchConfig, + crd::NodeRoles, + framework::{ClusterName, ProductVersion, role_utils::GenericProductSpecificCommonConfig}, + }; + + #[test] + pub fn test_environment_variables() { + let image: ProductImage = serde_json::from_str(r#"{"productVersion": "3.0.0"}"#) + .expect("should be a valid ProductImage"); + let cluster = ValidatedCluster { + metadata: ObjectMeta::default(), + image: image.clone(), + product_version: ProductVersion::from_str(image.product_version()) + .expect("should be a valid ProductVersion"), + name: ClusterName::from_str("my-opensearch-cluster") + .expect("should be a valid ClusterName"), + namespace: "default".to_owned(), + uid: "0b1e30e6-326e-4c1a-868d-ad6598b49e8b".to_owned(), + role_config: GenericRoleConfig::default(), + role_group_configs: BTreeMap::new(), + }; + + let role_group_config = OpenSearchRoleGroupConfig { + replicas: 1, + config: ValidatedOpenSearchConfig { + affinity: StackableAffinity::default(), + node_roles: NodeRoles::default(), + resources: Resources::default(), + termination_grace_period_seconds: 30, + }, + config_overrides: HashMap::default(), + env_overrides: [("TEST".to_owned(), "value".to_owned())].into(), + cli_overrides: BTreeMap::default(), + pod_overrides: PodTemplateSpec::default(), + product_specific_common_config: GenericProductSpecificCommonConfig::default(), + }; + + let node_config = NodeConfig::new( + cluster, + role_group_config, + "my-opensearch-cluster-manager".to_owned(), + ); + + let env_vars = node_config.environment_variables(); + + // TODO Test EnvVarSet and compare EnvVarSets + assert_eq!( + vec![ + EnvVar { + name: "TEST".to_owned(), + value: Some("value".to_owned()), + value_from: None + }, + EnvVar { + name: "cluster.initial_cluster_manager_nodes".to_owned(), + value: Some("".to_owned()), + value_from: None + }, + EnvVar { + name: "discovery.seed_hosts".to_owned(), + value: Some("my-opensearch-cluster-manager".to_owned()), + value_from: None + }, + EnvVar { + name: "node.name".to_owned(), + value: None, + value_from: Some(EnvVarSource { + config_map_key_ref: None, + field_ref: Some(ObjectFieldSelector { + api_version: None, + field_path: "metadata.name".to_owned() + }), + resource_field_ref: None, + secret_key_ref: None + }) + }, + EnvVar { + name: "node.roles".to_owned(), + value: Some("".to_owned()), + value_from: None + } + ], + >>::into(env_vars) + ); + } +} diff --git a/rust/operator-binary/src/controller/build/role_builder.rs b/rust/operator-binary/src/controller/build/role_builder.rs new file mode 100644 index 0000000..6f3b44d --- /dev/null +++ b/rust/operator-binary/src/controller/build/role_builder.rs @@ -0,0 +1,198 @@ +use stackable_operator::{ + builder::meta::ObjectMetaBuilder, + k8s_openapi::{ + Resource, + api::{ + core::v1::{Service, ServiceAccount, ServicePort, ServiceSpec}, + policy::v1::PodDisruptionBudget, + rbac::v1::{ClusterRole, RoleBinding, RoleRef, Subject}, + }, + }, + kube::api::ObjectMeta, + kvp::{ + Label, Labels, + consts::{STACKABLE_VENDOR_KEY, STACKABLE_VENDOR_VALUE}, + }, +}; + +use super::role_group_builder::{ + HTTP_PORT, HTTP_PORT_NAME, RoleGroupBuilder, TRANSPORT_PORT, TRANSPORT_PORT_NAME, +}; +use crate::{ + controller::{ContextNames, ValidatedCluster}, + framework::{ + IsLabelValue, + builder::{ + meta::ownerreference_from_resource, pdb::pod_disruption_budget_builder_with_role, + }, + role_utils::ResourceNames, + }, +}; + +const PDB_DEFAULT_MAX_UNAVAILABLE: u16 = 1; + +pub struct RoleBuilder<'a> { + cluster: ValidatedCluster, + context_names: &'a ContextNames, + resource_names: ResourceNames, +} + +impl<'a> RoleBuilder<'a> { + pub fn new(cluster: ValidatedCluster, context_names: &'a ContextNames) -> RoleBuilder<'a> { + RoleBuilder { + cluster: cluster.clone(), + context_names, + resource_names: ResourceNames { + cluster_name: cluster.name.clone(), + product_name: context_names.product_name.clone(), + }, + } + } + + // TODO Only one builder function which calls the other ones? + + pub fn role_group_builders(&self) -> Vec { + self.cluster + .role_group_configs + .iter() + .map(|(role_group_name, role_group_config)| { + RoleGroupBuilder::new( + self.resource_names.service_account_name(), + self.cluster.clone(), + role_group_name.clone(), + role_group_config.clone(), + self.context_names, + self.resource_names.discovery_service_name(), + ) + }) + .collect() + } + + pub fn build_service_account(&self) -> ServiceAccount { + let metadata = self.common_metadata(self.resource_names.service_account_name()); + + ServiceAccount { + metadata, + ..ServiceAccount::default() + } + } + + pub fn build_role_binding(&self) -> RoleBinding { + let metadata = self.common_metadata(self.resource_names.role_binding_name()); + + RoleBinding { + metadata, + role_ref: RoleRef { + api_group: ClusterRole::GROUP.to_owned(), + kind: ClusterRole::KIND.to_owned(), + name: self.resource_names.cluster_role_name(), + }, + subjects: Some(vec![Subject { + api_group: Some(ServiceAccount::GROUP.to_owned()), + kind: ServiceAccount::KIND.to_owned(), + name: self.resource_names.service_account_name(), + namespace: Some(self.cluster.namespace.clone()), + }]), + } + } + + pub fn build_cluster_manager_service(&self) -> Service { + let ports = vec![ + ServicePort { + name: Some(HTTP_PORT_NAME.to_owned()), + port: HTTP_PORT.into(), + ..ServicePort::default() + }, + ServicePort { + name: Some(TRANSPORT_PORT_NAME.to_owned()), + port: TRANSPORT_PORT.into(), + ..ServicePort::default() + }, + ]; + + let metadata = self.common_metadata(self.resource_names.discovery_service_name()); + + let service_selector = + RoleGroupBuilder::cluster_manager_labels(&self.cluster, self.context_names); + + let service_spec = ServiceSpec { + // Internal communication does not need to be exposed + type_: Some("ClusterIP".to_string()), + cluster_ip: Some("None".to_string()), + ports: Some(ports), + selector: Some(service_selector.into()), + publish_not_ready_addresses: Some(true), + ..ServiceSpec::default() + }; + + Service { + metadata, + spec: Some(service_spec), + status: None, + } + } + + pub fn build_pdb(&self) -> Option { + let pdb_config = &self.cluster.role_config.pod_disruption_budget; + + if pdb_config.enabled { + let max_unavailable = pdb_config + .max_unavailable + .unwrap_or(PDB_DEFAULT_MAX_UNAVAILABLE); + Some( + pod_disruption_budget_builder_with_role( + &self.cluster, + &self.context_names.product_name, + &ValidatedCluster::role_name(), + &self.context_names.operator_name, + &self.context_names.controller_name, + ) + .with_max_unavailable(max_unavailable) + .build(), + ) + } else { + None + } + } + + fn common_metadata(&self, resource_name: impl Into) -> ObjectMeta { + ObjectMetaBuilder::new() + .name(resource_name) + .namespace(&self.cluster.namespace) + .ownerreference(ownerreference_from_resource( + &self.cluster, + None, + Some(true), + )) + .with_labels(self.labels()) + .build() + } + + /// Labels on role resources + fn labels(&self) -> Labels { + // Well-known Kubernetes labels + let mut labels = Labels::role_selector( + &self.cluster, + &self.context_names.product_name.to_label_value(), + &ValidatedCluster::role_name().to_label_value(), + ) + .unwrap(); + + let managed_by = Label::managed_by( + &self.context_names.operator_name.to_string(), + &self.context_names.controller_name.to_string(), + ) + .unwrap(); + let version = Label::version(&self.cluster.product_version.to_string()).unwrap(); + + labels.insert(managed_by); + labels.insert(version); + + // Stackable-specific labels + labels + .parse_insert((STACKABLE_VENDOR_KEY, STACKABLE_VENDOR_VALUE)) + .unwrap(); + + labels + } +} diff --git a/rust/operator-binary/src/controller/build/role_group_builder.rs b/rust/operator-binary/src/controller/build/role_group_builder.rs new file mode 100644 index 0000000..6ca788a --- /dev/null +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -0,0 +1,378 @@ +use stackable_operator::{ + builder::{meta::ObjectMetaBuilder, pod::container::ContainerBuilder}, + k8s_openapi::{ + DeepMerge, + api::{ + apps::v1::{StatefulSet, StatefulSetSpec}, + core::v1::{ + Affinity, ConfigMap, ConfigMapVolumeSource, Container, ContainerPort, + PodSecurityContext, PodSpec, PodTemplateSpec, Probe, Service, ServicePort, + ServiceSpec, TCPSocketAction, Volume, VolumeMount, + }, + }, + apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString}, + }, + kube::api::ObjectMeta, + kvp::{Label, Labels}, +}; + +use super::node_config::{CONFIGURATION_FILE_OPENSEARCH_YML, NodeConfig}; +use crate::{ + controller::{ContextNames, OpenSearchRoleGroupConfig, ValidatedCluster}, + crd::v1alpha1, + framework::{ + RoleGroupName, + builder::meta::ownerreference_from_resource, + kvp::label::{recommended_labels, role_group_selector, role_selector}, + role_group_utils::ResourceNames, + }, +}; + +pub const HTTP_PORT_NAME: &str = "http"; +pub const HTTP_PORT: u16 = 9200; +pub const TRANSPORT_PORT_NAME: &str = "transport"; +pub const TRANSPORT_PORT: u16 = 9300; + +const CONFIG_VOLUME_NAME: &str = "config"; +const DATA_VOLUME_NAME: &str = "data"; + +// Path in opensearchproject/opensearch:3.0.0 +const OPENSEARCH_BASE_PATH: &str = "/usr/share/opensearch"; + +pub struct RoleGroupBuilder<'a> { + service_account_name: String, + cluster: ValidatedCluster, + node_config: NodeConfig, + role_group_name: RoleGroupName, + role_group_config: OpenSearchRoleGroupConfig, + context_names: &'a ContextNames, + resource_names: ResourceNames, +} + +impl<'a> RoleGroupBuilder<'a> { + pub fn new( + service_account_name: String, + cluster: ValidatedCluster, + role_group_name: RoleGroupName, + role_group_config: OpenSearchRoleGroupConfig, + context_names: &'a ContextNames, + discovery_service_name: String, + ) -> RoleGroupBuilder<'a> { + RoleGroupBuilder { + service_account_name, + cluster: cluster.clone(), + node_config: NodeConfig::new( + cluster.clone(), + role_group_config.clone(), + discovery_service_name, + ), + role_group_name: role_group_name.clone(), + role_group_config, + context_names, + resource_names: ResourceNames { + cluster_name: cluster.name.clone(), + role_name: ValidatedCluster::role_name(), + role_group_name, + }, + } + } + + pub fn build_config_map(&self) -> ConfigMap { + let metadata = + self.common_metadata(self.resource_names.role_group_config_map(), Labels::new()); + + let data = [( + CONFIGURATION_FILE_OPENSEARCH_YML.to_owned(), + self.node_config.static_opensearch_config(), + )] + .into(); + + ConfigMap { + metadata, + data: Some(data), + ..ConfigMap::default() + } + } + + pub fn build_stateful_set(&self) -> StatefulSet { + let metadata = self.common_metadata(self.resource_names.stateful_set_name(), Labels::new()); + + let template = self.build_pod_template(); + + let data_volume_claim_template = self + .role_group_config + .config + .resources + .storage + .data + .build_pvc(DATA_VOLUME_NAME, Some(vec!["ReadWriteOnce"])); + + let spec = StatefulSetSpec { + // Order does not matter for OpenSearch + pod_management_policy: Some("Parallel".to_string()), + replicas: Some(self.role_group_config.replicas.into()), + selector: LabelSelector { + match_labels: Some(self.pod_selector().into()), + ..LabelSelector::default() + }, + service_name: Some(self.resource_names.headless_service_name()), + template, + volume_claim_templates: Some(vec![data_volume_claim_template]), + ..StatefulSetSpec::default() + }; + + StatefulSet { + metadata, + spec: Some(spec), + status: None, + } + } + + fn build_pod_template(&self) -> PodTemplateSpec { + let mut node_role_labels = Labels::new(); + for node_role in self.role_group_config.config.node_roles.iter() { + node_role_labels.insert(Self::build_node_role_label(node_role)); + } + + let metadata = ObjectMetaBuilder::new() + .with_labels(self.recommended_labels()) + .with_labels(node_role_labels) + .build(); + + let container = self.build_container(&self.role_group_config); + + // The PodBuilder is not used because it re-validates the values which are already + // validated. For instance, it would be necessary to convert the + // termination_grace_period_seconds into a Duration, the PodBuilder parses the Duration, + // converts it back into seconds and fails if this is not possible. + let mut pod_template = PodTemplateSpec { + metadata: Some(metadata), + spec: Some(PodSpec { + affinity: Some(Affinity { + node_affinity: self.role_group_config.config.affinity.node_affinity.clone(), + pod_affinity: self.role_group_config.config.affinity.pod_affinity.clone(), + pod_anti_affinity: self + .role_group_config + .config + .affinity + .pod_anti_affinity + .clone(), + }), + containers: vec![container], + node_selector: self + .role_group_config + .config + .affinity + .node_selector + .clone() + .map(|wrapped| wrapped.node_selector), + security_context: Some(PodSecurityContext { + fs_group: Some(1000), + ..PodSecurityContext::default() + }), + service_account_name: Some(self.service_account_name.clone()), + termination_grace_period_seconds: Some( + self.role_group_config + .config + .termination_grace_period_seconds, + ), + volumes: Some(vec![Volume { + name: CONFIG_VOLUME_NAME.to_owned(), + config_map: Some(ConfigMapVolumeSource { + name: self.resource_names.role_group_config_map(), + ..Default::default() + }), + ..Volume::default() + }]), + ..PodSpec::default() + }), + }; + + pod_template.merge_from(self.role_group_config.pod_overrides.clone()); + + pod_template + } + + pub fn cluster_manager_labels( + cluster: &ValidatedCluster, + context_names: &ContextNames, + ) -> Labels { + let mut labels = role_selector( + cluster, + &context_names.product_name, + &ValidatedCluster::role_name(), + ); + + labels.insert(Self::build_node_role_label( + &v1alpha1::NodeRole::ClusterManager, + )); + + labels + } + + fn build_node_role_label(node_role: &v1alpha1::NodeRole) -> Label { + // TODO Check the maximum length at compile-time + Label::try_from(( + format!("stackable.tech/opensearch-role.{node_role}"), + "true".to_string(), + )) + .expect("should be a valid label") + } + + fn build_container(&self, role_group_config: &OpenSearchRoleGroupConfig) -> Container { + let product_image = self + .cluster + .image + .resolve("opensearch", crate::built_info::PKG_VERSION); + + // Probe values taken from the official Helm chart + let startup_probe = Probe { + failure_threshold: Some(30), + initial_delay_seconds: Some(5), + period_seconds: Some(10), + tcp_socket: Some(TCPSocketAction { + port: IntOrString::String(HTTP_PORT_NAME.to_owned()), + ..TCPSocketAction::default() + }), + timeout_seconds: Some(3), + ..Probe::default() + }; + let readiness_probe = Probe { + failure_threshold: Some(3), + period_seconds: Some(5), + tcp_socket: Some(TCPSocketAction { + port: IntOrString::String(HTTP_PORT_NAME.to_owned()), + ..TCPSocketAction::default() + }), + timeout_seconds: Some(3), + ..Probe::default() + }; + + ContainerBuilder::new("opensearch") + .expect("should be a valid container name") + .image_from_product_image(&product_image) + .command(vec![format!( + "{OPENSEARCH_BASE_PATH}/opensearch-docker-entrypoint.sh" + )]) + .args(role_group_config.cli_overrides_to_vec()) + .add_env_vars(self.node_config.environment_variables().into()) + .add_volume_mounts([ + VolumeMount { + mount_path: format!( + "{OPENSEARCH_BASE_PATH}/config/{CONFIGURATION_FILE_OPENSEARCH_YML}" + ), + name: CONFIG_VOLUME_NAME.to_owned(), + read_only: Some(true), + sub_path: Some(CONFIGURATION_FILE_OPENSEARCH_YML.to_owned()), + ..VolumeMount::default() + }, + VolumeMount { + mount_path: format!("{OPENSEARCH_BASE_PATH}/data"), + name: DATA_VOLUME_NAME.to_owned(), + ..VolumeMount::default() + }, + ]) + .expect("The mount paths are statically defined and there should be no duplicates.") + .add_container_ports(vec![ + ContainerPort { + name: Some(HTTP_PORT_NAME.to_owned()), + container_port: HTTP_PORT.into(), + ..ContainerPort::default() + }, + ContainerPort { + name: Some(TRANSPORT_PORT_NAME.to_owned()), + container_port: TRANSPORT_PORT.into(), + ..ContainerPort::default() + }, + ]) + .resources(self.role_group_config.config.resources.clone().into()) + .startup_probe(startup_probe) + .readiness_probe(readiness_probe) + .build() + } + + pub fn build_headless_service(&self) -> Service { + let ports = vec![ + ServicePort { + name: Some(HTTP_PORT_NAME.to_owned()), + port: HTTP_PORT.into(), + ..ServicePort::default() + }, + ServicePort { + name: Some(TRANSPORT_PORT_NAME.to_owned()), + port: TRANSPORT_PORT.into(), + ..ServicePort::default() + }, + ]; + + self.build_role_group_service( + self.resource_names.headless_service_name(), + ports, + Labels::new(), + ) + } + + fn build_role_group_service( + &self, + service_name: impl Into, + ports: Vec, + extra_labels: Labels, + ) -> Service { + let metadata = self.common_metadata(service_name, extra_labels); + + let service_spec = ServiceSpec { + // Internal communication does not need to be exposed + type_: Some("ClusterIP".to_string()), + cluster_ip: Some("None".to_string()), + ports: Some(ports), + selector: Some(self.pod_selector().into()), + publish_not_ready_addresses: Some(true), + ..ServiceSpec::default() + }; + + Service { + metadata, + spec: Some(service_spec), + status: None, + } + } + + fn common_metadata( + &self, + resource_name: impl Into, + extra_labels: Labels, + ) -> ObjectMeta { + ObjectMetaBuilder::new() + .name(resource_name) + .namespace(&self.cluster.namespace) + .ownerreference(ownerreference_from_resource( + &self.cluster, + None, + Some(true), + )) + .with_labels(self.recommended_labels()) + .with_labels(extra_labels) + .build() + } + + fn recommended_labels(&self) -> Labels { + recommended_labels( + &self.cluster, + &self.context_names.product_name, + &self.cluster.product_version, + &self.context_names.operator_name, + &self.context_names.controller_name, + &ValidatedCluster::role_name(), + &self.role_group_name, + ) + } + + fn pod_selector(&self) -> Labels { + role_group_selector( + &self.cluster, + &self.context_names.product_name, + &ValidatedCluster::role_name(), + &self.role_group_name, + ) + } +} diff --git a/rust/operator-binary/src/controller/update_status.rs b/rust/operator-binary/src/controller/update_status.rs new file mode 100644 index 0000000..923f7db --- /dev/null +++ b/rust/operator-binary/src/controller/update_status.rs @@ -0,0 +1,66 @@ +use snafu::{ResultExt, Snafu}; +use stackable_operator::{ + client::Client, + status::condition::{ + compute_conditions, operations::ClusterOperationsConditionBuilder, + statefulset::StatefulSetConditionBuilder, + }, +}; +use strum::{EnumDiscriminants, IntoStaticStr}; + +use super::{Applied, ContextNames, KubernetesResources}; +use crate::crd::v1alpha1::{self, OpenSearchClusterStatus}; + +#[derive(Snafu, Debug, EnumDiscriminants)] +#[strum_discriminants(derive(IntoStaticStr))] +pub enum Error { + #[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 update status"))] + UpdateStatus { + source: stackable_operator::client::Error, + }, +} + +type Result = std::result::Result; + +pub async fn update_status( + client: &Client, + names: &ContextNames, + cluster: &v1alpha1::OpenSearchCluster, + applied_resources: KubernetesResources, +) -> Result<()> { + let mut stateful_set_condition_builder = StatefulSetConditionBuilder::default(); + for stateful_set in applied_resources.stateful_sets { + stateful_set_condition_builder.add(stateful_set); + } + + let cluster_operation_cond_builder = + ClusterOperationsConditionBuilder::new(&cluster.spec.cluster_operation); + + let status = OpenSearchClusterStatus { + conditions: compute_conditions( + cluster, + &[ + &stateful_set_condition_builder, + &cluster_operation_cond_builder, + ], + ), + discovery_hash: None, + }; + + client + .apply_patch_status(&format!("{}", names.operator_name), cluster, &status) + .await + .context(UpdateStatusSnafu)?; + + Ok(()) +} diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs new file mode 100644 index 0000000..d171898 --- /dev/null +++ b/rust/operator-binary/src/controller/validate.rs @@ -0,0 +1,137 @@ +use std::{collections::BTreeMap, num::TryFromIntError, str::FromStr}; + +use snafu::{OptionExt, ResultExt, Snafu}; +use stackable_operator::{ + kube::{Resource, ResourceExt}, + role_utils::RoleGroup, + time::Duration, +}; +use strum::{EnumDiscriminants, IntoStaticStr}; + +use super::{ + ContextNames, OpenSearchRoleGroupConfig, ProductVersion, RoleGroupName, ValidatedCluster, + ValidatedOpenSearchConfig, +}; +use crate::{ + crd::v1alpha1::{self, OpenSearchConfig, OpenSearchConfigFragment}, + framework::{ + ClusterName, + role_utils::{GenericProductSpecificCommonConfig, RoleGroupConfig, with_validated_config}, + }, +}; + +#[derive(Snafu, Debug, EnumDiscriminants)] +#[strum_discriminants(derive(IntoStaticStr))] +pub enum Error { + #[snafu(display("failed to get the cluster name"))] + GetClusterName {}, + + #[snafu(display("failed to get the cluster namespace"))] + GetClusterNamespace {}, + + #[snafu(display("failed to get the cluster UID"))] + GetClusterUid {}, + + #[snafu(display("failed to set cluster name"))] + ParseClusterName { source: crate::framework::Error }, + + #[snafu(display("failed to set product version"))] + ParseProductVersion { source: crate::framework::Error }, + + #[snafu(display("failed to set role-group name"))] + ParseRoleGroupName { source: crate::framework::Error }, + + #[snafu(display("fragment validation failure"))] + ValidateOpenSearchConfig { + source: stackable_operator::config::fragment::ValidationError, + }, + + #[snafu(display("termination grace period is too long (got {duration}, maximum allowed is {max})", max = Duration::from_secs(i64::MAX as u64)))] + TerminationGracePeriodTooLong { + source: TryFromIntError, + duration: Duration, + }, +} + +type Result = std::result::Result; + +// no client needed +pub fn validate( + context_names: &ContextNames, + cluster: &v1alpha1::OpenSearchCluster, +) -> Result { + let raw_cluster_name = cluster.meta().name.clone().context(GetClusterNameSnafu)?; + let cluster_name = ClusterName::from_str(&raw_cluster_name).context(ParseClusterNameSnafu)?; + + let namespace = cluster.namespace().context(GetClusterNamespaceSnafu)?; + + let uid = cluster.uid().context(GetClusterUidSnafu)?; + + let product_version = ProductVersion::from_str(cluster.spec.image.product_version()) + .context(ParseProductVersionSnafu)?; + + let mut role_group_configs = BTreeMap::new(); + for (raw_role_group_name, role_group_config) in &cluster.spec.nodes.role_groups { + let role_group_name = + RoleGroupName::from_str(raw_role_group_name).context(ParseRoleGroupNameSnafu)?; + + let validated_role_group_config = + validate_role_group_config(context_names, &cluster_name, cluster, role_group_config)?; + + role_group_configs.insert(role_group_name, validated_role_group_config); + } + + Ok(ValidatedCluster { + metadata: cluster.meta().to_owned(), + image: cluster.spec.image.clone(), + product_version, + name: cluster_name, + namespace, + uid, + role_config: cluster.spec.nodes.role_config.clone(), + role_group_configs, + }) +} + +fn validate_role_group_config( + context_names: &ContextNames, + cluster_name: &ClusterName, + cluster: &v1alpha1::OpenSearchCluster, + role_group_config: &RoleGroup, +) -> Result { + let merged_role_group: RoleGroup = with_validated_config( + role_group_config, + &cluster.spec.nodes, + &v1alpha1::OpenSearchConfig::default_config( + &context_names.product_name, + cluster_name, + &ValidatedCluster::role_name(), + ), + ) + .context(ValidateOpenSearchConfigSnafu)?; + + let graceful_shutdown_timeout = merged_role_group.config.config.graceful_shutdown_timeout; + let termination_grace_period_seconds = graceful_shutdown_timeout.as_secs().try_into().context( + TerminationGracePeriodTooLongSnafu { + duration: graceful_shutdown_timeout, + }, + )?; + + let validated_config = ValidatedOpenSearchConfig { + affinity: merged_role_group.config.config.affinity, + node_roles: merged_role_group.config.config.node_roles, + resources: merged_role_group.config.config.resources, + termination_grace_period_seconds, + }; + + Ok(RoleGroupConfig { + // Kubernetes defaults to 1 if not set + replicas: merged_role_group.replicas.unwrap_or(1), + config: validated_config, + config_overrides: merged_role_group.config.config_overrides, + env_overrides: merged_role_group.config.env_overrides, + cli_overrides: merged_role_group.config.cli_overrides, + pod_overrides: merged_role_group.config.pod_overrides, + product_specific_common_config: merged_role_group.config.product_specific_common_config, + }) +} diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 42cc2c2..1c5c69d 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -1,13 +1,38 @@ +use std::{slice, str::FromStr}; + use serde::{Deserialize, Serialize}; use stackable_operator::{ + commons::{ + affinity::{StackableAffinity, StackableAffinityFragment, affinity_between_role_pods}, + cluster_operation::ClusterOperation, + product_image_selection::ProductImage, + resources::{ + CpuLimitsFragment, MemoryLimitsFragment, NoRuntimeLimitsFragment, PvcConfig, + PvcConfigFragment, Resources, ResourcesFragment, + }, + }, + config::{ + fragment::Fragment, + merge::{Atomic, Merge}, + }, + k8s_openapi::{api::core::v1::PodAntiAffinity, apimachinery::pkg::api::resource::Quantity}, kube::CustomResource, + role_utils::{GenericRoleConfig, Role}, schemars::{self, JsonSchema}, - status::condition::ClusterCondition, + status::condition::{ClusterCondition, HasStatusCondition}, + time::Duration, versioned::versioned, }; +use strum::Display; + +use crate::framework::{ + ClusterName, IsLabelValue, ProductName, RoleName, + role_utils::GenericProductSpecificCommonConfig, +}; #[versioned(version(name = "v1alpha1"))] pub mod versioned { + /// A OpenSearch cluster stacklet. This resource is managed by the Stackable operator for OpenSearch. /// Find more information on how to use it and the resources that the operator generates in the /// [operator documentation](DOCS_BASE_URL_PLACEHOLDER/opensearch/). @@ -26,7 +51,103 @@ pub mod versioned { ) ))] #[serde(rename_all = "camelCase")] - pub struct OpenSearchClusterSpec {} + pub struct OpenSearchClusterSpec { + // no doc string - see ProductImage struct + pub image: ProductImage, + + // no doc string - see ClusterOperation struct + #[serde(default)] + pub cluster_operation: ClusterOperation, + + /// OpenSearch nodes + pub nodes: + Role, + } + + // The possible node roles are by default the built-in roles and the search role, see + // https://github.com/opensearch-project/OpenSearch/blob/3.0.0/server/src/main/java/org/opensearch/cluster/node/DiscoveryNode.java#L609-L614. + // + // Plugins can set additional roles, see + // https://github.com/opensearch-project/OpenSearch/blob/3.0.0/server/src/main/java/org/opensearch/cluster/node/DiscoveryNode.java#L629-L646. + // + // For instance, the ml-commons plugin adds the node role "ml", see + // https://github.com/opensearch-project/ml-commons/blob/3.0.0.0/plugin/src/main/java/org/opensearch/ml/plugin/MachineLearningPlugin.java#L394. + // If such a plugin is added, then this enumeration must be extended accordingly. + #[derive( + Clone, Debug, Deserialize, Display, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, + )] + // The OpenSearch configuration uses snake_case. To make it easier to match the log output of + // OpenSearch with this cluster configuration, snake_case is also used here. + #[serde(rename_all = "snake_case")] + pub enum NodeRole { + // Built-in node roles + // see https://github.com/opensearch-project/OpenSearch/blob/3.0.0/server/src/main/java/org/opensearch/cluster/node/DiscoveryNodeRole.java#L341-L346 + + // TODO https://github.com/Peternator7/strum/issues/113 + #[strum(serialize = "data")] + Data, + #[strum(serialize = "ingest")] + Ingest, + #[strum(serialize = "cluster_manager")] + ClusterManager, + #[strum(serialize = "remote_cluster_client")] + RemoteClusterClient, + #[strum(serialize = "warm")] + Warm, + + // Search node role + // see https://github.com/opensearch-project/OpenSearch/blob/3.0.0/server/src/main/java/org/opensearch/cluster/node/DiscoveryNodeRole.java#L313-L339 + #[strum(serialize = "search")] + Search, + } + + #[derive(Clone, Debug, Fragment, JsonSchema, PartialEq)] + #[fragment_attrs( + derive( + Clone, + Debug, + Default, + Deserialize, + Merge, + JsonSchema, + PartialEq, + Serialize + ), + serde(rename_all = "camelCase") + )] + pub struct OpenSearchConfig { + #[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: Duration, + + pub node_roles: NodeRoles, + + #[fragment_attrs(serde(default))] + pub resources: Resources, + } + + #[derive(Clone, Debug, Default, JsonSchema, PartialEq, Fragment)] + #[fragment_attrs( + derive( + Clone, + Debug, + Default, + Deserialize, + Merge, + JsonSchema, + PartialEq, + Serialize + ), + serde(rename_all = "camelCase") + )] + pub struct StorageConfig { + #[fragment_attrs(serde(default))] + pub data: PvcConfig, + } #[derive(Clone, Default, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] @@ -39,5 +160,91 @@ pub mod versioned { } } -#[cfg(test)] -mod tests {} +impl HasStatusCondition for v1alpha1::OpenSearchCluster { + fn conditions(&self) -> Vec { + match &self.status { + Some(status) => status.conditions.clone(), + None => vec![], + } + } +} + +impl v1alpha1::OpenSearchConfig { + pub fn default_config( + product_name: &ProductName, + cluster_name: &ClusterName, + role_name: &RoleName, + ) -> v1alpha1::OpenSearchConfigFragment { + v1alpha1::OpenSearchConfigFragment { + affinity: StackableAffinityFragment { + pod_affinity: None, + pod_anti_affinity: Some(PodAntiAffinity { + preferred_during_scheduling_ignored_during_execution: Some(vec![ + affinity_between_role_pods( + &product_name.to_label_value(), + &cluster_name.to_label_value(), + &role_name.to_label_value(), + 1, + ), + ]), + required_during_scheduling_ignored_during_execution: None, + }), + node_affinity: None, + node_selector: None, + }, + // Default taken from the Helm chart, see + // https://github.com/opensearch-project/helm-charts/blob/opensearch-3.0.0/charts/opensearch/values.yaml#L364 + graceful_shutdown_timeout: Some( + Duration::from_str("2m").expect("should be a valid duration"), + ), + // Defaults taken from the Helm chart, see + // https://github.com/opensearch-project/helm-charts/blob/opensearch-3.0.0/charts/opensearch/values.yaml#L16-L20 + node_roles: Some(NodeRoles(vec![ + v1alpha1::NodeRole::ClusterManager, + v1alpha1::NodeRole::Ingest, + v1alpha1::NodeRole::Data, + v1alpha1::NodeRole::RemoteClusterClient, + ])), + resources: ResourcesFragment { + memory: MemoryLimitsFragment { + // An idle node already requires 2 Gi. + limit: Some(Quantity("2Gi".to_owned())), + runtime_limits: NoRuntimeLimitsFragment {}, + }, + cpu: CpuLimitsFragment { + // Default taken from the Helm chart, see + // https://github.com/opensearch-project/helm-charts/blob/opensearch-3.0.0/charts/opensearch/values.yaml#L150 + min: Some(Quantity("1".to_owned())), + // an arbitrary value + max: Some(Quantity("4".to_owned())), + }, + storage: v1alpha1::StorageConfigFragment { + data: PvcConfigFragment { + // Default taken from the Helm chart, see + // https://github.com/opensearch-project/helm-charts/blob/opensearch-3.0.0/charts/opensearch/values.yaml#L220 + // This value should be overriden by the user. Data nodes need probably + // more, the other nodes less. + capacity: Some(Quantity("8Gi".to_owned())), + storage_class: None, + selectors: None, + }, + }, + }, + } + } +} + +#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] +pub struct NodeRoles(Vec); + +impl NodeRoles { + pub fn contains(&self, node_role: &v1alpha1::NodeRole) -> bool { + self.0.contains(node_role) + } + + pub fn iter(&self) -> slice::Iter { + self.0.iter() + } +} + +impl Atomic for NodeRoles {} diff --git a/rust/operator-binary/src/framework.rs b/rust/operator-binary/src/framework.rs new file mode 100644 index 0000000..aad601d --- /dev/null +++ b/rust/operator-binary/src/framework.rs @@ -0,0 +1,192 @@ +// Type-safe wrappers that cannot throw errors +// The point is, to move the validation "upwards". +// The contents of this module will be moved to operator-rs when stabilized. + +use std::{fmt::Display, str::FromStr}; + +use kvp::label::MAX_LABEL_VALUE_LENGTH; +use snafu::{ResultExt, Snafu, ensure}; +use stackable_operator::kvp::LabelValue; +use strum::{EnumDiscriminants, IntoStaticStr}; + +pub mod builder; +pub mod cluster_resources; +pub mod kvp; +pub mod role_group_utils; +pub mod role_utils; + +#[derive(Snafu, Debug, EnumDiscriminants)] +#[strum_discriminants(derive(IntoStaticStr))] +pub enum Error { + #[snafu(display("maximum length exceeded"))] + LengthExceeded { length: usize, max_length: usize }, + + #[snafu(display("object name not RFC 1123 compliant"))] + InvalidObjectName { + source: stackable_operator::validation::Errors, + }, + + #[snafu(display("failed to use as label"))] + InvalidLabelValue { + source: stackable_operator::kvp::LabelValueError, + }, +} + +// TODO The maximum length of objects differs. +/// Maximum length of a DNS subdomain name as defined in RFC 1123. +#[allow(dead_code)] +pub const MAX_OBJECT_NAME_LENGTH: usize = 253; + +/// Has a name that can be used as a DNS subdomain name as defined in RFC 1123. +/// Most resource types, e.g. a Pod, require such a compliant name. +pub trait HasObjectName { + fn to_object_name(&self) -> String; +} + +/// Has a namespace +pub trait HasNamespace { + fn to_namespace(&self) -> String; +} + +/// Has a Kubernetes UID +pub trait HasUid { + fn to_uid(&self) -> String; +} + +/// Is a valid label value as defined in RFC 1123. +pub trait IsLabelValue { + fn to_label_value(&self) -> String; +} + +/// Restricted string type with attributes like maximum length. +macro_rules! attributed_string_type { + ($name:ident, $description:literal $(, $attribute:tt)*) => { + #[doc = $description] + #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] + pub struct $name(String); + + impl Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } + } + + impl FromStr for $name { + type Err = Error; + + fn from_str(s: &str) -> std::result::Result { + + $(attributed_string_type!(@from_str $name, s, $attribute);)* + + Ok(Self(s.to_owned())) + } + } + + $(attributed_string_type!(@trait_impl $name, $attribute);)* + }; + (@from_str $name:ident, $s:expr, (max_length = $max_length:expr)) => { + let length = $s.len() as usize; + ensure!( + length <= $name::MAX_LENGTH, + LengthExceededSnafu { + length, + max_length: $name::MAX_LENGTH, + } + ); + }; + (@from_str $name:ident, $s:expr, is_object_name) => { + stackable_operator::validation::is_rfc_1123_subdomain($s).context(InvalidObjectNameSnafu)?; + }; + (@from_str $name:ident, $s:expr, is_valid_label_value) => { + LabelValue::from_str($s).context(InvalidLabelValueSnafu)?; + }; + (@trait_impl $name:ident, (max_length = $max_length:expr)) => { + impl $name { + // type arithmetic would be better + pub const MAX_LENGTH: usize = $max_length; + } + }; + (@trait_impl $name:ident, is_object_name) => { + impl HasObjectName for $name { + fn to_object_name(&self) -> String { + self.0.clone() + } + } + }; + (@trait_impl $name:ident, is_valid_label_value) => { + impl IsLabelValue for $name { + fn to_label_value(&self) -> String { + self.0.clone() + } + } + }; +} + +attributed_string_type! { + ProductName, + "The name of a product, e.g. \"opensearch\"", + // A suffix is added to produce a label value. An according compile-time check ensures that + // max_length cannot be set higher. + (max_length = 54), + is_valid_label_value +} +attributed_string_type! { + ProductVersion, + "The version of a product, e.g. \"3.0.0\"", + (max_length = MAX_LABEL_VALUE_LENGTH), + is_valid_label_value +} +attributed_string_type! { + ClusterName, + "The name of a cluster/stacklet, e.g. \"my-opensearch-cluster\"", + // Suffixes are added to produce a resource names. According compile-time check ensures that + // max_length cannot be set higher. + (max_length = 24), + is_object_name, + is_valid_label_value +} +attributed_string_type! { + ControllerName, + "The name of a controller in an operator, e.g. \"opensearchcluster\"", + (max_length = MAX_LABEL_VALUE_LENGTH), + is_valid_label_value +} +attributed_string_type! { + OperatorName, + "The name of an operator, e.g. \"opensearch.stackable.tech\"", + (max_length = MAX_LABEL_VALUE_LENGTH), + is_valid_label_value +} +attributed_string_type! { + RoleGroupName, + "The name of a role-group name, e.g. \"cluster-manager\"", + (max_length = 16), + is_object_name, + is_valid_label_value +} +attributed_string_type! { + RoleName, + "The name of a role name, e.g. \"nodes\"", + (max_length = 10), + is_object_name, + is_valid_label_value +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use crate::framework::ProductName; + + #[test] + fn test_object_name_constraints() { + assert!(ProductName::from_str("valid-role-group-name").is_ok()); + assert!(ProductName::from_str("invalid-character: /").is_err()); + assert!( + ProductName::from_str( + "too-long-123456789012345678901234567890123456789012345678901234567890" + ) + .is_err() + ); + } +} diff --git a/rust/operator-binary/src/framework/builder.rs b/rust/operator-binary/src/framework/builder.rs new file mode 100644 index 0000000..40caba1 --- /dev/null +++ b/rust/operator-binary/src/framework/builder.rs @@ -0,0 +1,3 @@ +pub mod meta; +pub mod pdb; +pub mod pod; diff --git a/rust/operator-binary/src/framework/builder/meta.rs b/rust/operator-binary/src/framework/builder/meta.rs new file mode 100644 index 0000000..56b5279 --- /dev/null +++ b/rust/operator-binary/src/framework/builder/meta.rs @@ -0,0 +1,23 @@ +use stackable_operator::{ + builder::meta::OwnerReferenceBuilder, + k8s_openapi::apimachinery::pkg::apis::meta::v1::OwnerReference, kube::Resource, +}; + +use crate::framework::HasUid; + +/// Infallible variant of `stackable_operator::builder::meta::ObjectMetaBuilder::ownerreference_from_resource` +pub fn ownerreference_from_resource( + resource: &(impl Resource + HasUid), + block_owner_deletion: Option, + controller: Option, +) -> OwnerReference { + OwnerReferenceBuilder::new() + // Set api_version, kind, name and additionally the UID if it exists. + .initialize_from_resource(resource) + // Ensure that the UID is set. + .uid(resource.to_uid()) + .block_owner_deletion_opt(block_owner_deletion) + .controller_opt(controller) + .build() + .expect("api_version, kind, name and uid should be set") +} diff --git a/rust/operator-binary/src/framework/builder/pdb.rs b/rust/operator-binary/src/framework/builder/pdb.rs new file mode 100644 index 0000000..0f3be90 --- /dev/null +++ b/rust/operator-binary/src/framework/builder/pdb.rs @@ -0,0 +1,24 @@ +use stackable_operator::{ + builder::pdb::PodDisruptionBudgetBuilder, + k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector, + kube::{Resource, api::ObjectMeta}, +}; + +use crate::framework::{ControllerName, IsLabelValue, OperatorName, ProductName, RoleName}; + +pub fn pod_disruption_budget_builder_with_role( + owner: &(impl Resource + IsLabelValue), + product_name: &ProductName, + role_name: &RoleName, + operator_name: &OperatorName, + controller_name: &ControllerName, +) -> PodDisruptionBudgetBuilder { + PodDisruptionBudgetBuilder::new_with_role( + owner, + &product_name.to_label_value(), + &role_name.to_label_value(), + &operator_name.to_label_value(), + &controller_name.to_label_value(), + ) + .expect("Labels should be created because all given parameters produce valid label values") +} diff --git a/rust/operator-binary/src/framework/builder/pod.rs b/rust/operator-binary/src/framework/builder/pod.rs new file mode 100644 index 0000000..18581c4 --- /dev/null +++ b/rust/operator-binary/src/framework/builder/pod.rs @@ -0,0 +1 @@ +pub mod container; diff --git a/rust/operator-binary/src/framework/builder/pod/container.rs b/rust/operator-binary/src/framework/builder/pod/container.rs new file mode 100644 index 0000000..4d4d482 --- /dev/null +++ b/rust/operator-binary/src/framework/builder/pod/container.rs @@ -0,0 +1,77 @@ +use std::collections::BTreeMap; + +use stackable_operator::{ + builder::pod::container::FieldPathEnvVar, + k8s_openapi::api::core::v1::{EnvVar, EnvVarSource, ObjectFieldSelector}, +}; + +// TODO Use validated type +type EnvVarName = String; + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct EnvVarSet(BTreeMap); + +impl EnvVarSet { + pub fn new() -> Self { + Self::default() + } + + pub fn with_values(self, env_vars: I) -> Self + where + I: IntoIterator, + K: Into, + V: Into, + { + env_vars + .into_iter() + .fold(self, |extended_env_vars, (name, value)| { + extended_env_vars.with_value(name, value) + }) + } + + pub fn with_value(mut self, name: impl Into, value: impl Into) -> Self { + let name: EnvVarName = name.into(); + + self.0.insert( + name.clone(), + EnvVar { + name, + value: Some(value.into()), + value_from: None, + }, + ); + + self + } + + pub fn with_field_path( + mut self, + name: impl Into, + field_path: FieldPathEnvVar, + ) -> Self { + let name: EnvVarName = name.into(); + + self.0.insert( + name.clone(), + EnvVar { + name, + value: None, + value_from: Some(EnvVarSource { + field_ref: Some(ObjectFieldSelector { + field_path: field_path.to_string(), + ..ObjectFieldSelector::default() + }), + ..EnvVarSource::default() + }), + }, + ); + + self + } +} + +impl From for Vec { + fn from(value: EnvVarSet) -> Self { + value.0.values().cloned().collect() + } +} diff --git a/rust/operator-binary/src/framework/cluster_resources.rs b/rust/operator-binary/src/framework/cluster_resources.rs new file mode 100644 index 0000000..3316be4 --- /dev/null +++ b/rust/operator-binary/src/framework/cluster_resources.rs @@ -0,0 +1,39 @@ +use stackable_operator::{ + cluster_resources::{ClusterResourceApplyStrategy, ClusterResources}, + k8s_openapi::api::core::v1::ObjectReference, +}; + +use super::{ + ControllerName, HasNamespace, HasObjectName, HasUid, IsLabelValue, OperatorName, ProductName, +}; +use crate::framework::kvp::label::MAX_LABEL_VALUE_LENGTH; + +pub fn cluster_resources_new( + product_name: &ProductName, + operator_name: &OperatorName, + controller_name: &ControllerName, + cluster: &(impl HasObjectName + HasNamespace + HasUid), + apply_strategy: ClusterResourceApplyStrategy, +) -> ClusterResources { + // ClusterResources::new creates a label value from the given app name by appending + // `-operator`. For the resulting label value to be valid, it must not exceed 63 characters. + // Check at compile time that ProductName::MAX_LENGTH is defined accordingly. + const _: () = assert!( + ProductName::MAX_LENGTH + "-operator".len() <= MAX_LABEL_VALUE_LENGTH, + "The label value `-operator` must not exceed 63 characters." + ); + + ClusterResources::new( + &product_name.to_label_value(), + &operator_name.to_label_value(), + &controller_name.to_label_value(), + &ObjectReference { + name: Some(cluster.to_object_name()), + namespace: Some(cluster.to_namespace()), + uid: Some(cluster.to_uid()), + ..Default::default() + }, + apply_strategy, + ) + .expect("ClusterResources should be created because the cluster object reference contains name, namespace and uid.") +} diff --git a/rust/operator-binary/src/framework/kvp.rs b/rust/operator-binary/src/framework/kvp.rs new file mode 100644 index 0000000..0006163 --- /dev/null +++ b/rust/operator-binary/src/framework/kvp.rs @@ -0,0 +1 @@ +pub mod label; diff --git a/rust/operator-binary/src/framework/kvp/label.rs b/rust/operator-binary/src/framework/kvp/label.rs new file mode 100644 index 0000000..ce875ea --- /dev/null +++ b/rust/operator-binary/src/framework/kvp/label.rs @@ -0,0 +1,64 @@ +use stackable_operator::{ + kube::Resource, + kvp::{Labels, ObjectLabels}, +}; + +use crate::framework::{ + ControllerName, IsLabelValue, OperatorName, ProductName, ProductVersion, RoleGroupName, + RoleName, +}; + +pub const MAX_LABEL_VALUE_LENGTH: usize = 63; + +/// Infallible variant of `Labels::recommended` +pub fn recommended_labels( + owner: &(impl Resource + IsLabelValue), + product_name: &ProductName, + product_version: &ProductVersion, + operator_name: &OperatorName, + controller_name: &ControllerName, + role_name: &RoleName, + role_group_name: &RoleGroupName, +) -> Labels { + let object_labels = ObjectLabels { + owner, + app_name: &product_name.to_label_value(), + app_version: &product_version.to_label_value(), + operator_name: &operator_name.to_label_value(), + controller_name: &controller_name.to_label_value(), + role: &role_name.to_label_value(), + role_group: &role_group_name.to_label_value(), + }; + Labels::recommended(object_labels) + .expect("Labels should be created because all given parameters produce valid label values") +} + +/// Infallible variant of `Labels::role_selector` +pub fn role_selector( + owner: &(impl Resource + IsLabelValue), + product_name: &ProductName, + role_name: &RoleName, +) -> Labels { + Labels::role_selector( + owner, + &product_name.to_label_value(), + &role_name.to_label_value(), + ) + .expect("Labels should be created because all given parameters produce valid label values") +} + +/// Infallible variant of `Labels::role_group_selector` +pub fn role_group_selector( + owner: &(impl Resource + IsLabelValue), + product_name: &ProductName, + role_name: &RoleName, + role_group_name: &RoleGroupName, +) -> Labels { + Labels::role_group_selector( + owner, + &product_name.to_label_value(), + &role_name.to_label_value(), + &role_group_name.to_label_value(), + ) + .expect("Labels should be created because all given parameters produce valid label values") +} diff --git a/rust/operator-binary/src/framework/role_group_utils.rs b/rust/operator-binary/src/framework/role_group_utils.rs new file mode 100644 index 0000000..e4b522a --- /dev/null +++ b/rust/operator-binary/src/framework/role_group_utils.rs @@ -0,0 +1,92 @@ +use super::{ClusterName, RoleGroupName, RoleName}; +use crate::framework::{HasObjectName, MAX_OBJECT_NAME_LENGTH, kvp::label::MAX_LABEL_VALUE_LENGTH}; + +pub struct ResourceNames { + pub cluster_name: ClusterName, + pub role_name: RoleName, + pub role_group_name: RoleGroupName, +} + +impl ResourceNames { + // used at compile-time + #[allow(dead_code)] + const MAX_QUALIFIED_ROLE_GROUP_NAME_LENGTH: usize = ClusterName::MAX_LENGTH + + 1 // dash + + RoleName::MAX_LENGTH + + 1 // dash + + RoleGroupName::MAX_LENGTH; + + /// Creates a qualified role group name consisting of the cluster name, role name and role-group + /// name. + /// The result is a valid DNS subdomain name as defined in RFC 1123 that can be used e.g. as a name + /// for a StatefulSet. + fn qualified_role_group_name(&self) -> String { + format!( + "{}-{}-{}", + self.cluster_name.to_object_name(), + self.role_name.to_object_name(), + self.role_group_name.to_object_name() + ) + } + + pub fn role_group_config_map(&self) -> String { + // Compile-time check + const _: () = assert!( + ResourceNames::MAX_QUALIFIED_ROLE_GROUP_NAME_LENGTH <= MAX_OBJECT_NAME_LENGTH, + "The ConfigMap name `--` must not exceed 253 characters." + ); + + self.qualified_role_group_name() + } + + pub fn stateful_set_name(&self) -> String { + // Compile-time check + const _: () = assert!( + // see https://github.com/kubernetes/kubernetes/issues/64023 + ResourceNames::MAX_QUALIFIED_ROLE_GROUP_NAME_LENGTH + + 1 // dash + + 10 // digits for the controller-revision-hash label + <= MAX_LABEL_VALUE_LENGTH, + "The maximum lengths of the cluster name, role name and role group name must be defined so that the combination of these names (including separators and the sequential pod number or hash) is also a valid object name with a maximum of 63 characters (see RFC 1123)" + ); + + self.qualified_role_group_name() + } + + pub fn headless_service_name(&self) -> String { + const SUFFIX: &str = "-headless"; + + // Compile-time check + const _: () = assert!( + ResourceNames::MAX_QUALIFIED_ROLE_GROUP_NAME_LENGTH + SUFFIX.len() + <= MAX_LABEL_VALUE_LENGTH, + "The Service name `---headless` must not exceed 63 characters." + ); + + format!("{}{SUFFIX}", self.qualified_role_group_name()) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::{ClusterName, RoleGroupName, RoleName}; + use crate::framework::role_group_utils::ResourceNames; + + #[test] + fn test_stateful_set_name() { + let resource_names = ResourceNames { + cluster_name: ClusterName::from_str("test-cluster") + .expect("should be a valid cluster name"), + role_name: RoleName::from_str("data-nodes").expect("should be a valid role name"), + role_group_name: RoleGroupName::from_str("ssd-storage") + .expect("should be a valid role group name"), + }; + + assert_eq!( + "test-cluster-data-nodes-ssd-storage", + resource_names.stateful_set_name() + ); + } +} diff --git a/rust/operator-binary/src/framework/role_utils.rs b/rust/operator-binary/src/framework/role_utils.rs new file mode 100644 index 0000000..db0c5d7 --- /dev/null +++ b/rust/operator-binary/src/framework/role_utils.rs @@ -0,0 +1,203 @@ +use std::collections::{BTreeMap, HashMap}; + +use serde::{Deserialize, Serialize}; +use stackable_operator::{ + config::{ + fragment::{self, FromFragment}, + merge::Merge, + }, + k8s_openapi::api::core::v1::PodTemplateSpec, + role_utils::{CommonConfiguration, Role, RoleGroup}, + schemars::JsonSchema, +}; + +use super::ProductName; +use crate::framework::{ClusterName, MAX_OBJECT_NAME_LENGTH, kvp::label::MAX_LABEL_VALUE_LENGTH}; + +#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] +pub struct GenericProductSpecificCommonConfig {} + +impl Merge for GenericProductSpecificCommonConfig { + fn merge(&mut self, _defaults: &Self) {} +} + +// much better to work with than RoleGroup +#[derive(Clone, Debug, PartialEq)] +pub struct RoleGroupConfig { + pub replicas: u16, + pub config: T, + pub config_overrides: HashMap>, + pub env_overrides: HashMap, + pub cli_overrides: BTreeMap, + pub pod_overrides: PodTemplateSpec, + // allow(dead_code) is not necessary anymore when moved to operator-rs + #[allow(dead_code)] + pub product_specific_common_config: ProductSpecificCommonConfig, +} + +impl RoleGroupConfig { + pub fn cli_overrides_to_vec(&self) -> Vec { + self.cli_overrides + .clone() + .into_iter() + .flat_map(|(option, value)| [option, value]) + .collect() + } +} + +// RoleGroup::validate_config with fixed types +pub fn validate_config( + role_group: &RoleGroup, + role: &Role, + default_config: &T, +) -> Result +where + C: FromFragment, + ProductSpecificCommonConfig: Default + JsonSchema + Serialize, + T: Merge + Clone, + U: Default + JsonSchema + Serialize, +{ + let mut role_config = role.config.config.clone(); + role_config.merge(default_config); + let mut rolegroup_config = role_group.config.config.clone(); + rolegroup_config.merge(&role_config); + fragment::validate(rolegroup_config) +} + +// also useful for operators which use the product config +pub fn with_validated_config( + role_group: &RoleGroup, + role: &Role, + default_config: &T, +) -> Result, fragment::ValidationError> +where + C: FromFragment, + ProductSpecificCommonConfig: Clone + Default + JsonSchema + Merge + Serialize, + T: Clone + Merge, + U: Default + JsonSchema + Serialize, +{ + let validated_config = validate_config(role_group, role, default_config)?; + Ok(RoleGroup { + config: CommonConfiguration { + config: validated_config, + config_overrides: merged_config_overrides( + role.config.config_overrides.clone(), + role_group.config.config_overrides.clone(), + ), + env_overrides: merged_env_overrides( + role.config.env_overrides.clone(), + role_group.config.env_overrides.clone(), + ), + cli_overrides: merged_cli_overrides( + role.config.cli_overrides.clone(), + role_group.config.cli_overrides.clone(), + ), + pod_overrides: merged_pod_overrides( + role.config.pod_overrides.clone(), + role_group.config.pod_overrides.clone(), + ), + product_specific_common_config: merged_product_specific_common_config( + role.config.product_specific_common_config.clone(), + role_group.config.product_specific_common_config.clone(), + ), + }, + replicas: role_group.replicas, + }) +} + +fn merged_config_overrides( + role_config_overrides: HashMap>, + role_group_config_overrides: HashMap>, +) -> HashMap> { + let mut merged_config_overrides = role_config_overrides; + + for (filename, role_group_config_file_overrides) in role_group_config_overrides { + merged_config_overrides + .entry(filename) + .or_default() + .extend(role_group_config_file_overrides); + } + + merged_config_overrides +} + +fn merged_env_overrides( + role_env_overrides: HashMap, + role_group_env_overrides: HashMap, +) -> HashMap { + let mut merged_env_overrides = role_env_overrides; + merged_env_overrides.extend(role_group_env_overrides); + merged_env_overrides +} + +fn merged_cli_overrides( + role_cli_overrides: BTreeMap, + role_group_cli_overrides: BTreeMap, +) -> BTreeMap { + let mut merged_cli_overrides = role_cli_overrides; + merged_cli_overrides.extend(role_group_cli_overrides); + merged_cli_overrides +} + +fn merged_pod_overrides( + role_pod_overrides: PodTemplateSpec, + role_group_pod_overrides: PodTemplateSpec, +) -> PodTemplateSpec { + let mut merged_pod_overrides = role_group_pod_overrides; + merged_pod_overrides.merge(&role_pod_overrides); + merged_pod_overrides +} + +fn merged_product_specific_common_config(role_config: T, role_group_config: T) -> T +where + T: Merge, +{ + let mut merged_config = role_group_config; + merged_config.merge(&role_config); + merged_config +} + +pub struct ResourceNames { + pub cluster_name: ClusterName, + pub product_name: ProductName, +} + +impl ResourceNames { + pub fn service_account_name(&self) -> String { + const SUFFIX: &str = "-serviceaccount"; + + // Compile-time check + const _: () = assert!( + ClusterName::MAX_LENGTH + SUFFIX.len() <= MAX_OBJECT_NAME_LENGTH, + "The ServiceAccount name `-serviceaccount` must not exceed 253 characters." + ); + + format!("{}{SUFFIX}", self.cluster_name) + } + + pub fn role_binding_name(&self) -> String { + const SUFFIX: &str = "-rolebinding"; + + // No compile-time check, because RoleBinding names do not seem to be restricted. + + format!("{}{SUFFIX}", self.cluster_name) + } + + pub fn cluster_role_name(&self) -> String { + const SUFFIX: &str = "-clusterrole"; + + // No compile-time check, because ClusterRole names do not seem to be restricted. + + format!("{}{SUFFIX}", self.product_name) + } + + pub fn discovery_service_name(&self) -> String { + // Compile-time check + const _: () = assert!( + ClusterName::MAX_LENGTH <= MAX_LABEL_VALUE_LENGTH, + "The Service name `` must not exceed 63 characters." + ); + + format!("{}", self.cluster_name) + } +} diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index 978c3d0..730a607 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -1,15 +1,31 @@ +use std::{str::FromStr, sync::Arc}; + use clap::Parser as _; -use crd::OpenSearchCluster; +use crd::{OpenSearchCluster, v1alpha1}; +use framework::OperatorName; +use futures::StreamExt; use snafu::{ResultExt as _, Snafu}; use stackable_operator::{ YamlSchema as _, cli::{Command, ProductOperatorRun}, + k8s_openapi::api::{apps::v1::StatefulSet, core::v1::Service}, + kube::{ + core::DeserializeGuard, + runtime::{ + Controller, + events::{Recorder, Reporter}, + watcher, + }, + }, + logging::controller::report_controller_reconciled, shared::yaml::SerializeOptions, telemetry::Tracing, }; use strum::{EnumDiscriminants, IntoStaticStr}; +mod controller; mod crd; +mod framework; mod built_info { include!(concat!(env!("OUT_DIR"), "/built.rs")); @@ -35,6 +51,11 @@ pub enum Error { SerializeCrd { source: stackable_operator::shared::yaml::Error, }, + + #[snafu(display("failed to create Kubernetes client"))] + CreateClient { + source: stackable_operator::client::Error, + }, } #[derive(clap::Parser)] @@ -57,9 +78,9 @@ async fn main() -> Result<()> { } Command::Run(ProductOperatorRun { product_config: _, - watch_namespace: _, + watch_namespace, telemetry_arguments, - cluster_info_opts: _, + cluster_info_opts, }) => { let _tracing_guard = Tracing::pre_configured(built_info::PKG_NAME, telemetry_arguments) .init() @@ -74,6 +95,65 @@ async fn main() -> Result<()> { "Starting {description}", description = built_info::PKG_DESCRIPTION ); + + let operator_name = OperatorName::from_str("opensearch.stackable.tech") + .expect("should be a valid operator name"); + + let client = stackable_operator::client::initialize_operator( + Some(format!("{operator_name}")), + &cluster_info_opts, + ) + .await + .context(CreateClientSnafu)?; + + let controller_context = controller::Context::new(client.clone(), operator_name); + let full_controller_name = controller_context.full_controller_name(); + + let event_recorder = Arc::new(Recorder::new( + client.as_kube_client(), + Reporter { + controller: full_controller_name.clone(), + instance: None, + }, + )); + + let controller = Controller::new( + watch_namespace.get_api::>(&client), + watcher::Config::default(), + ); + controller + .owns( + watch_namespace.get_api::(&client), + watcher::Config::default(), + ) + .owns( + watch_namespace.get_api::(&client), + watcher::Config::default(), + ) + .shutdown_on_signal() + .run( + controller::reconcile, + controller::error_policy, + Arc::new(controller_context), + ) + .for_each_concurrent( + 16, // concurrency limit + |result| { + // The event_recorder needs to be shared across all invocations, so that + // events are correctly aggregated + let event_recorder = event_recorder.clone(); + let full_controller_name = full_controller_name.clone(); + async move { + report_controller_reconciled( + &event_recorder, + &full_controller_name, + &result, + ) + .await; + } + }, + ) + .await; } } diff --git a/tests/release.yaml b/tests/release.yaml new file mode 100644 index 0000000..c877cd4 --- /dev/null +++ b/tests/release.yaml @@ -0,0 +1,16 @@ +# Contains all operators required to run the test suite. +--- +releases: + # Do not change the name of the release as it's referenced from run-tests + tests: + releaseDate: 1970-01-01 + description: Integration test + products: + commons: + operatorVersion: 0.0.0-dev + secret: + operatorVersion: 0.0.0-dev + listener: + operatorVersion: 0.0.0-dev + opensearch: + operatorVersion: 0.0.0-dev diff --git a/tests/templates/kuttl/smoke/00-patch-ns.yaml b/tests/templates/kuttl/smoke/00-patch-ns.yaml new file mode 100644 index 0000000..d4f91fa --- /dev/null +++ b/tests/templates/kuttl/smoke/00-patch-ns.yaml @@ -0,0 +1,15 @@ +# see https://github.com/stackabletech/issues/issues/566 +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: | + kubectl patch namespace $NAMESPACE --patch=' + { + "metadata": { + "labels": { + "pod-security.kubernetes.io/enforce": "privileged" + } + } + }' + timeout: 120 diff --git a/tests/templates/kuttl/smoke/01-rbac.yaml b/tests/templates/kuttl/smoke/01-rbac.yaml new file mode 100644 index 0000000..64eced8 --- /dev/null +++ b/tests/templates/kuttl/smoke/01-rbac.yaml @@ -0,0 +1,31 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: test-service-account +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: test-role +rules: + - apiGroups: + - security.openshift.io + resources: + - securitycontextconstraints + resourceNames: + - privileged + verbs: + - use +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: test-role-binding +subjects: + - kind: ServiceAccount + name: test-service-account +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: test-role diff --git a/tests/templates/kuttl/smoke/02-limit-range.yaml b/tests/templates/kuttl/smoke/02-limit-range.yaml new file mode 100644 index 0000000..b1789b2 --- /dev/null +++ b/tests/templates/kuttl/smoke/02-limit-range.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: v1 +kind: LimitRange +metadata: + name: limit-request-ratio +spec: + limits: + - type: Container + maxLimitRequestRatio: + cpu: 5 + memory: 1 diff --git a/tests/templates/kuttl/smoke/10-assert.yaml b/tests/templates/kuttl/smoke/10-assert.yaml new file mode 100644 index 0000000..eb06201 --- /dev/null +++ b/tests/templates/kuttl/smoke/10-assert.yaml @@ -0,0 +1,537 @@ +# All fields are checked that are set by the operator. +# This helps to detect unintentional changes. +# The maintenance effort should be okay as long as it is only done in the smoke test. +# TODO Check individual field in unit tests +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 600 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: cluster-manager + app.kubernetes.io/version: 3.0.0 + stackable.tech/vendor: Stackable + name: opensearch-nodes-cluster-manager + ownerReferences: + - apiVersion: opensearch.stackable.tech/v1alpha1 + controller: true + kind: OpenSearchCluster + name: opensearch +spec: + podManagementPolicy: Parallel + replicas: 3 + selector: + matchLabels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: cluster-manager + serviceName: opensearch-nodes-cluster-manager-headless + template: + metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: cluster-manager + app.kubernetes.io/version: 3.0.0 + stackable.tech/opensearch-role.cluster_manager: "true" + stackable.tech/vendor: Stackable + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/name: opensearch + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - command: + - /usr/share/opensearch/opensearch-docker-entrypoint.sh + env: + - name: DISABLE_INSTALL_DEMO_CONFIG + value: "true" + - name: cluster.initial_cluster_manager_nodes + value: opensearch-nodes-cluster-manager-0,opensearch-nodes-cluster-manager-1,opensearch-nodes-cluster-manager-2 + - name: discovery.seed_hosts + value: opensearch + - name: node.name + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.name + - name: node.roles + value: cluster_manager + image: opensearchproject/opensearch:3.0.0 + imagePullPolicy: Always + name: opensearch + ports: + - containerPort: 9200 + name: http + protocol: TCP + - containerPort: 9300 + name: transport + protocol: TCP + readinessProbe: + failureThreshold: 3 + periodSeconds: 5 + successThreshold: 1 + tcpSocket: + port: http + timeoutSeconds: 3 + resources: + limits: + cpu: "4" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + startupProbe: + failureThreshold: 30 + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + tcpSocket: + port: http + timeoutSeconds: 3 + volumeMounts: + - mountPath: /usr/share/opensearch/config/opensearch.yml + name: config + readOnly: true + subPath: opensearch.yml + - mountPath: /usr/share/opensearch/data + name: data + - mountPath: /usr/share/opensearch/config/opensearch-security + name: security-config + readOnly: true + - mountPath: /usr/share/opensearch/config/tls + name: tls + readOnly: true + securityContext: + fsGroup: 1000 + serviceAccount: opensearch-serviceaccount + serviceAccountName: opensearch-serviceaccount + terminationGracePeriodSeconds: 120 + volumes: + - configMap: + defaultMode: 420 + name: opensearch-nodes-cluster-manager + name: config + - ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls + secrets.stackable.tech/scope: node,pod,service=opensearch-cluster-manager,service=opensearch-nodes-cluster-manager + creationTimestamp: null + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" + storageClassName: secrets.stackable.tech + volumeMode: Filesystem + name: tls + - name: security-config + secret: + defaultMode: 420 + secretName: opensearch-security-config + volumeClaimTemplates: + - apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + name: data + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Mi + volumeMode: Filesystem + status: + phase: Pending +status: + readyReplicas: 3 + replicas: 3 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: data + app.kubernetes.io/version: 3.0.0 + stackable.tech/vendor: Stackable + name: opensearch-nodes-data + ownerReferences: + - apiVersion: opensearch.stackable.tech/v1alpha1 + controller: true + kind: OpenSearchCluster + name: opensearch +spec: + podManagementPolicy: Parallel + replicas: 2 + selector: + matchLabels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: data + serviceName: opensearch-nodes-data-headless + template: + metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: data + app.kubernetes.io/version: 3.0.0 + stackable.tech/opensearch-role.data: "true" + stackable.tech/opensearch-role.ingest: "true" + stackable.tech/opensearch-role.remote_cluster_client: "true" + stackable.tech/vendor: Stackable + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/name: opensearch + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - command: + - /usr/share/opensearch/opensearch-docker-entrypoint.sh + env: + - name: DISABLE_INSTALL_DEMO_CONFIG + value: "true" + - name: cluster.initial_cluster_manager_nodes + - name: discovery.seed_hosts + value: opensearch + - name: node.name + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.name + - name: node.roles + value: ingest,data,remote_cluster_client + image: opensearchproject/opensearch:3.0.0 + imagePullPolicy: Always + name: opensearch + ports: + - containerPort: 9200 + name: http + protocol: TCP + - containerPort: 9300 + name: transport + protocol: TCP + readinessProbe: + failureThreshold: 3 + periodSeconds: 5 + successThreshold: 1 + tcpSocket: + port: http + timeoutSeconds: 3 + resources: + limits: + cpu: "4" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + startupProbe: + failureThreshold: 30 + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + tcpSocket: + port: http + timeoutSeconds: 3 + volumeMounts: + - mountPath: /usr/share/opensearch/config/opensearch.yml + name: config + readOnly: true + subPath: opensearch.yml + - mountPath: /usr/share/opensearch/data + name: data + - mountPath: /usr/share/opensearch/config/opensearch-security + name: security-config + readOnly: true + - mountPath: /usr/share/opensearch/config/tls + name: tls + readOnly: true + securityContext: + fsGroup: 1000 + serviceAccount: opensearch-serviceaccount + serviceAccountName: opensearch-serviceaccount + terminationGracePeriodSeconds: 120 + volumes: + - configMap: + defaultMode: 420 + name: opensearch-nodes-data + name: config + - ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls + secrets.stackable.tech/scope: node,pod,service=opensearch-nodes-data + creationTimestamp: null + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" + storageClassName: secrets.stackable.tech + volumeMode: Filesystem + name: tls + - name: security-config + secret: + defaultMode: 420 + secretName: opensearch-security-config + volumeClaimTemplates: + - apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + name: data + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi + volumeMode: Filesystem + status: + phase: Pending +status: + readyReplicas: 2 + replicas: 2 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: cluster-manager + app.kubernetes.io/version: 3.0.0 + stackable.tech/vendor: Stackable + name: opensearch-nodes-cluster-manager +data: + opensearch.yml: |- + cluster.name: "opensearch" + discovery.type: "zen" + network.host: "0.0.0.0" + plugins.security.allow_default_init_securityindex: "true" + plugins.security.nodes_dn: ["CN=generated certificate for pod"] + plugins.security.ssl.http.enabled: "true" + plugins.security.ssl.http.pemcert_filepath: "/usr/share/opensearch/config/tls/tls.crt" + plugins.security.ssl.http.pemkey_filepath: "/usr/share/opensearch/config/tls/tls.key" + plugins.security.ssl.http.pemtrustedcas_filepath: "/usr/share/opensearch/config/tls/ca.crt" + plugins.security.ssl.transport.enabled: "true" + plugins.security.ssl.transport.pemcert_filepath: "/usr/share/opensearch/config/tls/tls.crt" + plugins.security.ssl.transport.pemkey_filepath: "/usr/share/opensearch/config/tls/tls.key" + plugins.security.ssl.transport.pemtrustedcas_filepath: "/usr/share/opensearch/config/tls/ca.crt" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: data + app.kubernetes.io/version: 3.0.0 + stackable.tech/vendor: Stackable + name: opensearch-nodes-data +data: + opensearch.yml: |- + cluster.name: "opensearch" + discovery.type: "zen" + network.host: "0.0.0.0" + plugins.security.allow_default_init_securityindex: "true" + plugins.security.nodes_dn: ["CN=generated certificate for pod"] + plugins.security.ssl.http.enabled: "true" + plugins.security.ssl.http.pemcert_filepath: "/usr/share/opensearch/config/tls/tls.crt" + plugins.security.ssl.http.pemkey_filepath: "/usr/share/opensearch/config/tls/tls.key" + plugins.security.ssl.http.pemtrustedcas_filepath: "/usr/share/opensearch/config/tls/ca.crt" + plugins.security.ssl.transport.enabled: "true" + plugins.security.ssl.transport.pemcert_filepath: "/usr/share/opensearch/config/tls/tls.crt" + plugins.security.ssl.transport.pemkey_filepath: "/usr/share/opensearch/config/tls/tls.key" + plugins.security.ssl.transport.pemtrustedcas_filepath: "/usr/share/opensearch/config/tls/ca.crt" +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: cluster-manager + app.kubernetes.io/version: 3.0.0 + stackable.tech/vendor: Stackable + name: opensearch-nodes-cluster-manager-headless +spec: + ports: + - name: http + port: 9200 + protocol: TCP + targetPort: 9200 + - name: transport + port: 9300 + protocol: TCP + targetPort: 9300 + publishNotReadyAddresses: true + selector: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: cluster-manager + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: data + app.kubernetes.io/version: 3.0.0 + stackable.tech/vendor: Stackable + name: opensearch-nodes-data-headless +spec: + ports: + - name: http + port: 9200 + protocol: TCP + targetPort: 9200 + - name: transport + port: 9300 + protocol: TCP + targetPort: 9300 + publishNotReadyAddresses: true + selector: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: data + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + app.kubernetes.io/version: 3.0.0 + stackable.tech/vendor: Stackable + name: opensearch + ownerReferences: + - apiVersion: opensearch.stackable.tech/v1alpha1 + controller: true + kind: OpenSearchCluster + name: opensearch +spec: + ports: + - name: http + port: 9200 + protocol: TCP + targetPort: 9200 + - name: transport + port: 9300 + protocol: TCP + targetPort: 9300 + publishNotReadyAddresses: true + selector: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/name: opensearch + stackable.tech/opensearch-role.cluster_manager: "true" + type: ClusterIP +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + app.kubernetes.io/version: 3.0.0 + stackable.tech/vendor: Stackable + name: opensearch-serviceaccount + ownerReferences: + - apiVersion: opensearch.stackable.tech/v1alpha1 + controller: true + kind: OpenSearchCluster + name: opensearch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + app.kubernetes.io/version: 3.0.0 + stackable.tech/vendor: Stackable + name: opensearch-rolebinding + ownerReferences: + - apiVersion: opensearch.stackable.tech/v1alpha1 + controller: true + kind: OpenSearchCluster + name: opensearch +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: opensearch-clusterrole +subjects: +- kind: ServiceAccount + name: opensearch-serviceaccount +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + name: opensearch-nodes +spec: + maxUnavailable: 1 + selector: + matchLabels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/name: opensearch diff --git a/tests/templates/kuttl/smoke/10-install-opensearch.yaml b/tests/templates/kuttl/smoke/10-install-opensearch.yaml new file mode 100644 index 0000000..9d68547 --- /dev/null +++ b/tests/templates/kuttl/smoke/10-install-opensearch.yaml @@ -0,0 +1,192 @@ +--- +apiVersion: opensearch.stackable.tech/v1alpha1 +kind: OpenSearchCluster +metadata: + name: opensearch +spec: + image: + custom: opensearchproject/opensearch:3.0.0 + productVersion: 3.0.0 + nodes: + roleGroups: + cluster-manager: + config: + nodeRoles: + - cluster_manager + resources: + storage: + data: + capacity: 100Mi + replicas: 3 + podOverrides: + spec: + volumes: + - name: tls + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/scope: node,pod,service=opensearch-cluster-manager,service=opensearch-nodes-cluster-manager + data: + config: + nodeRoles: + - ingest + - data + - remote_cluster_client + resources: + storage: + data: + capacity: 2Gi + replicas: 2 + podOverrides: + spec: + volumes: + - name: tls + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/scope: node,pod,service=opensearch-nodes-data + envOverrides: + # TODO Make these the defaults in the image + DISABLE_INSTALL_DEMO_CONFIG: "true" + configOverrides: + # TODO Add the required options to the operator + opensearch.yml: + # TODO Check that this is safe despite the warning in the documentation + plugins.security.allow_default_init_securityindex: "true" + plugins.security.ssl.transport.enabled: "true" + plugins.security.ssl.transport.pemcert_filepath: /usr/share/opensearch/config/tls/tls.crt + plugins.security.ssl.transport.pemkey_filepath: /usr/share/opensearch/config/tls/tls.key + plugins.security.ssl.transport.pemtrustedcas_filepath: /usr/share/opensearch/config/tls/ca.crt + plugins.security.ssl.http.enabled: "true" + plugins.security.ssl.http.pemcert_filepath: /usr/share/opensearch/config/tls/tls.crt + plugins.security.ssl.http.pemkey_filepath: /usr/share/opensearch/config/tls/tls.key + plugins.security.ssl.http.pemtrustedcas_filepath: /usr/share/opensearch/config/tls/ca.crt + podOverrides: + spec: + containers: + - name: opensearch + volumeMounts: + - name: security-config + mountPath: /usr/share/opensearch/config/opensearch-security + readOnly: true + - name: tls + # The Java policy allows reading from /usr/share/opensearch/config. + mountPath: /usr/share/opensearch/config/tls + readOnly: true + securityContext: + fsGroup: 1000 + volumes: + - name: security-config + secret: + secretName: opensearch-security-config + - name: tls + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls + spec: + storageClassName: secrets.stackable.tech + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" +--- +apiVersion: v1 +kind: Secret +metadata: + name: opensearch-security-config +stringData: + action_groups.yml: | + --- + _meta: + type: actiongroups + config_version: 2 + allowlist.yml: | + --- + _meta: + type: allowlist + config_version: 2 + + config: + enabled: false + audit.yml: | + --- + _meta: + type: audit + config_version: 2 + + config: + enabled: false + config.yml: | + --- + _meta: + type: config + config_version: 2 + + config: + dynamic: + authc: + basic_internal_auth_domain: + description: Authenticate via HTTP Basic against internal users database + http_enabled: true + transport_enabled: true + order: 1 + http_authenticator: + type: basic + challenge: true + authentication_backend: + type: intern + authz: {} + internal_users.yml: | + --- + # The hash value is a bcrypt hash and can be generated with plugin/tools/hash.sh + + _meta: + type: internalusers + config_version: 2 + + admin: + hash: $2y$10$xRtHZFJ9QhG9GcYhRpAGpufCZYsk//nxsuel5URh0GWEBgmiI4Q/e + reserved: true + backend_roles: + - admin + description: OpenSearch admin user + + kibanaserver: + hash: $2y$10$vPgQ/6ilKDM5utawBqxoR.7euhVQ0qeGl8mPTeKhmFT475WUDrfQS + reserved: true + description: OpenSearch Dashboards user + nodes_dn.yml: | + --- + _meta: + type: nodesdn + config_version: 2 + roles.yml: | + --- + _meta: + type: roles + config_version: 2 + roles_mapping.yml: | + --- + _meta: + type: rolesmapping + config_version: 2 + + all_access: + reserved: false + backend_roles: + - admin + + kibana_server: + reserved: true + users: + - kibanaserver + tenants.yml: | + --- + _meta: + type: tenants + config_version: 2 diff --git a/tests/templates/kuttl/smoke/20-assert.yaml b/tests/templates/kuttl/smoke/20-assert.yaml new file mode 100644 index 0000000..bebbaa9 --- /dev/null +++ b/tests/templates/kuttl/smoke/20-assert.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 600 +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: test-opensearch +status: + succeeded: 1 diff --git a/tests/templates/kuttl/smoke/20-test-opensearch.yaml b/tests/templates/kuttl/smoke/20-test-opensearch.yaml new file mode 100644 index 0000000..25c927c --- /dev/null +++ b/tests/templates/kuttl/smoke/20-test-opensearch.yaml @@ -0,0 +1,168 @@ +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: test-opensearch +spec: + template: + spec: + containers: + - name: test-opensearch + image: oci.stackable.tech/sdp/testing-tools:0.2.0-stackable0.0.0-dev + command: + - /bin/bash + - -euxo + - pipefail + - -c + args: + - | + pip install opensearch-py + python scripts/test.py + env: + # required for pip install + - name: HOME + value: /stackable + volumeMounts: + - name: script + mountPath: /stackable/scripts + - name: tls + mountPath: /stackable/tls + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + resources: + requests: + memory: 128Mi + cpu: 100m + limits: + memory: 128Mi + cpu: 400m + volumes: + - name: script + configMap: + name: test-opensearch + - name: tls + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls + spec: + storageClassName: secrets.stackable.tech + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" + serviceAccountName: test-service-account + securityContext: + fsGroup: 1000 + restartPolicy: OnFailure +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-opensearch +data: + test.py: | + # https://docs.opensearch.org/docs/latest/clients/python-low-level/#sample-program + + from opensearchpy import OpenSearch + + # TODO Use a discovery ConfigMap + + host = 'opensearch' + port = 9200 + auth = ('admin', 'AJVFsGJBbpT6mChn') # For testing only. Don't store credentials in code. + ca_certs_path = '/stackable/tls/ca.crt' + + # Create the client with SSL/TLS enabled, but hostname verification disabled. + client = OpenSearch( + hosts = [{'host': host, 'port': port}], + http_compress = True, # enables gzip compression for request bodies + http_auth = auth, + # client_cert = client_cert_path, + # client_key = client_key_path, + use_ssl = True, + verify_certs = True, + ssl_assert_hostname = False, + ssl_show_warn = False, + ca_certs = ca_certs_path + ) + + # Create an index with non-default settings. + index_name = 'python-test-index' + index_body = { + 'settings': { + 'index': { + 'number_of_shards': 4 + } + } + } + + response = client.indices.create(index=index_name, body=index_body) + print('\nCreating index:') + print(response) + + # Add a document to the index. + document = { + 'title': 'Moneyball', + 'director': 'Bennett Miller', + 'year': '2011' + } + id = '1' + + response = client.index( + index = index_name, + body = document, + id = id, + refresh = True + ) + + print('\nAdding document:') + print(response) + + # Perform bulk operations + + movies = '{ "index" : { "_index" : "my-dsl-index", "_id" : "2" } } \n { "title" : "Interstellar", "director" : "Christopher Nolan", "year" : "2014"} \n { "create" : { "_index" : "my-dsl-index", "_id" : "3" } } \n { "title" : "Star Trek Beyond", "director" : "Justin Lin", "year" : "2015"} \n { "update" : {"_id" : "3", "_index" : "my-dsl-index" } } \n { "doc" : {"year" : "2016"} }' + + client.bulk(body=movies) + + # Search for the document. + q = 'miller' + query = { + 'size': 5, + 'query': { + 'multi_match': { + 'query': q, + 'fields': ['title^2', 'director'] + } + } + } + + response = client.search( + body = query, + index = index_name + ) + print('\nSearch results:') + print(response) + + # Delete the document. + response = client.delete( + index = index_name, + id = id + ) + + print('\nDeleting document:') + print(response) + + # Delete the index. + response = client.indices.delete( + index = index_name + ) + + print('\nDeleting index:') + print(response) diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml new file mode 100644 index 0000000..505167a --- /dev/null +++ b/tests/test-definition.yaml @@ -0,0 +1,34 @@ +--- +dimensions: + - name: opensearch + values: + - 3.0.0 + - name: openshift + values: + - "false" +tests: + - name: smoke + dimensions: + - opensearch + - openshift +suites: + - name: nightly + patch: + - dimensions: + - name: opensearch + expr: last + - name: smoke-latest + select: + - smoke + patch: + - dimensions: + - expr: last + - name: openshift + patch: + - dimensions: + - expr: last + - dimensions: + - name: openshift + expr: "true" + - name: opensearch + expr: last