diff --git a/Cargo.lock b/Cargo.lock index 8e909feef..a1bd10ab4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,9 +47,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anstream" -version = "0.6.19" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -77,29 +77,29 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "async-broadcast" @@ -214,9 +214,9 @@ dependencies = [ [[package]] name = "backon" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302eaff5357a264a2c42f127ecb8bac761cf99749fc3dc95677e2743991f99e7" +checksum = "592277618714fbcecda9a02ba7a8781f319d26532a88553bbacc77ba5d2b3a8d" dependencies = [ "fastrand", "gloo-timers", @@ -316,9 +316,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.29" +version = "1.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" +checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" dependencies = [ "shlex", ] @@ -341,9 +341,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.40" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +checksum = "1c1f056bae57e3e54c3375c41ff79619ddd13460a17d7438712bd0d83fda4ff8" dependencies = [ "clap_builder", "clap_derive", @@ -351,9 +351,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.40" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" dependencies = [ "anstream", "anstyle", @@ -363,9 +363,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.40" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" dependencies = [ "heck", "proc-macro2", @@ -478,9 +478,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -557,17 +557,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "delegate" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9b6483c2bbed26f97861cf57651d4f2b731964a28cd2257f934a4b452480d21" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - [[package]] name = "der" version = "0.7.10" @@ -601,6 +590,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "derive_more" version = "2.0.1" @@ -666,9 +666,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "ecdsa" @@ -784,9 +784,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.4.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", @@ -1004,9 +1004,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" @@ -1046,9 +1046,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -1065,9 +1065,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", @@ -1253,9 +1253,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ "base64", "bytes", @@ -1419,9 +1419,9 @@ dependencies = [ [[package]] name = "io-uring" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" dependencies = [ "bitflags", "cfg-if", @@ -1508,7 +1508,7 @@ dependencies = [ "pest_derive", "regex", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.14", ] [[package]] @@ -1592,7 +1592,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "thiserror 2.0.12", + "thiserror 2.0.14", "tokio", "tokio-util", "tower", @@ -1616,7 +1616,7 @@ dependencies = [ "serde", "serde-value", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.14", ] [[package]] @@ -1654,7 +1654,7 @@ dependencies = [ "pin-project", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.14", "tokio", "tokio-util", "tracing", @@ -1671,9 +1671,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.174" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libm" @@ -1856,7 +1856,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.12", + "thiserror 2.0.14", "tracing", ] @@ -1898,7 +1898,7 @@ dependencies = [ "opentelemetry_sdk", "prost", "reqwest", - "thiserror 2.0.12", + "thiserror 2.0.14", "tokio", "tonic", "tracing", @@ -1933,9 +1933,9 @@ dependencies = [ "futures-util", "opentelemetry", "percent-encoding", - "rand 0.9.1", + "rand 0.9.2", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.14", "tokio", "tokio-stream", ] @@ -2034,7 +2034,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", - "thiserror 2.0.12", + "thiserror 2.0.14", "ucd-trie", ] @@ -2150,9 +2150,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.35" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" +checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" dependencies = [ "proc-macro2", "syn 2.0.104", @@ -2178,9 +2178,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" dependencies = [ "unicode-ident", ] @@ -2252,9 +2252,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", @@ -2300,9 +2300,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.13" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags", ] @@ -2479,9 +2479,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc_version" @@ -2494,22 +2494,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "rustls" -version = "0.23.28" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ "log", "once_cell", @@ -2542,7 +2542,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.2.0", + "security-framework 3.3.0", ] [[package]] @@ -2565,9 +2565,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.3" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "ring", "rustls-pki-types", @@ -2576,9 +2576,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -2673,9 +2673,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" dependencies = [ "bitflags", "core-foundation 0.10.1", @@ -2743,9 +2743,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" dependencies = [ "itoa", "memchr", @@ -2836,9 +2836,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -2861,9 +2861,9 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" @@ -2915,12 +2915,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.10" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2954,7 +2954,7 @@ dependencies = [ "k8s-openapi", "kube", "p256", - "rand 0.9.1", + "rand 0.9.2", "rand_core 0.6.4", "rsa", "sha2", @@ -2975,7 +2975,7 @@ dependencies = [ "chrono", "clap", "const_format", - "delegate", + "derivative", "dockerfile-parser", "educe", "either", @@ -3131,23 +3131,22 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", "syn 2.0.104", ] @@ -3238,11 +3237,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.14", ] [[package]] @@ -3258,9 +3257,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" dependencies = [ "proc-macro2", "quote", @@ -3340,9 +3339,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.46.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", @@ -3354,7 +3353,7 @@ dependencies = [ "slab", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3391,9 +3390,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -3405,9 +3404,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.0" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f271e09bde39ab52250160a67e88577e0559ad77e9085de6e9051a2c4353f8f8" +checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" dependencies = [ "indexmap", "serde", @@ -3446,18 +3445,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5c1c469eda89749d2230d8156a5969a69ffe0d6d01200581cdc6110674d293e" +checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b679217f2848de74cabd3e8fc5e6d66f40b7da40f8e1954d92054d9010690fd5" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" [[package]] name = "tonic" @@ -3651,9 +3650,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "trybuild" -version = "1.0.106" +version = "1.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65af40ad689f2527aebbd37a0a816aea88ff5f774ceabe99de5be02f2f91dae2" +checksum = "32e257d7246e7a9fd015fb0b28b330a8d4142151a33f03e6a497754f4b1f6a8e" dependencies = [ "glob", "serde", @@ -3928,7 +3927,7 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.2", + "windows-targets 0.53.3", ] [[package]] @@ -3949,10 +3948,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -4061,9 +4061,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] @@ -4099,9 +4099,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" +checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" [[package]] name = "xtask" @@ -4213,9 +4213,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", diff --git a/Cargo.toml b/Cargo.toml index 1bc0bc32f..e26b8603a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ const_format = "0.2.33" const-oid = { version = "0.9.6", features = ["db"] } convert_case = "0.8.0" darling = "0.20.10" -delegate = "0.13.0" +derivative = "2.2.0" dockerfile-parser = "0.9.0" ecdsa = { version = "0.16.9", features = ["digest", "pem"] } educe = { version = "0.6.0", default-features = false, features = ["Clone", "Debug", "Default", "PartialEq", "Eq"] } diff --git a/crates/stackable-operator/CHANGELOG.md b/crates/stackable-operator/CHANGELOG.md index a8d66b5ee..dfc719199 100644 --- a/crates/stackable-operator/CHANGELOG.md +++ b/crates/stackable-operator/CHANGELOG.md @@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Changed + +- BREAKING: `KeyValuePairs` (and `Annotations`/`Labels`) is now an alias for `BTreeMap` ([#889]). + - Generally, this means that the API now behaves like a map rather than a set. For example, duplicate keys are no longer allowed (as was already documented before). + - Some `KeyValuePairs` methods have been renamed for certain use cases: + - `KeyValuePairs::insert(&mut self, kvp)`: use `::extend(&mut self, [kvp])` instead. + - `KeyValuePairs::try_from`: you may need to use `::try_from_iter` instead. + - `KeyValuePairs::contains_key`: unvalidated keys will need to use `::contains_str_key` instead. + - `Into>`: use `::to_unvalidated` instead. + - Well-known annotations have been moved from `kvp::Annotation` to `kvp::annotation::well_known`. + - Well-known labels have been moved from `kvp::Label` to `kvp::label::well_known`. + - Well-known label sets have been moved from `kvp::Labels` to `kvp::label::well_known::sets`. + +### Fixed + +- `KeyValuePairs` will now consistently use the last-written value for a given key ([#889]). + ## [0.94.0] - 2025-07-10 ### Added diff --git a/crates/stackable-operator/Cargo.toml b/crates/stackable-operator/Cargo.toml index 15b7bd78a..e615f1b35 100644 --- a/crates/stackable-operator/Cargo.toml +++ b/crates/stackable-operator/Cargo.toml @@ -23,7 +23,7 @@ stackable-versioned = { path = "../stackable-versioned" } chrono.workspace = true clap.workspace = true const_format.workspace = true -delegate.workspace = true +derivative.workspace = true dockerfile-parser.workspace = true either.workspace = true educe.workspace = true diff --git a/crates/stackable-operator/src/builder/meta.rs b/crates/stackable-operator/src/builder/meta.rs index ed1c3b20c..c6db77477 100644 --- a/crates/stackable-operator/src/builder/meta.rs +++ b/crates/stackable-operator/src/builder/meta.rs @@ -3,7 +3,10 @@ use kube::{Resource, ResourceExt}; use snafu::{ResultExt, Snafu}; use tracing::warn; -use crate::kvp::{Annotation, Annotations, Label, LabelError, Labels, ObjectLabels}; +use crate::kvp::{ + Annotation, Annotations, KeyValuePairs, KeyValuePairsExt, Label, LabelError, Labels, + ObjectLabels, label, +}; type Result = std::result::Result; @@ -106,7 +109,7 @@ impl ObjectMetaBuilder { pub fn with_annotation(&mut self, annotation: Annotation) -> &mut Self { self.annotations .get_or_insert(Annotations::new()) - .insert(annotation); + .extend([annotation]); self } @@ -128,7 +131,7 @@ impl ObjectMetaBuilder { /// This adds a single label to the existing labels. /// It'll override a label with the same key. pub fn with_label(&mut self, label: Label) -> &mut Self { - self.labels.get_or_insert(Labels::new()).insert(label); + self.labels.get_or_insert(Labels::new()).extend([label]); self } @@ -154,7 +157,7 @@ impl ObjectMetaBuilder { object_labels: ObjectLabels, ) -> Result<&mut Self> { let recommended_labels = - Labels::recommended(object_labels).context(RecommendedLabelsSnafu)?; + label::well_known::sets::recommended(object_labels).context(RecommendedLabelsSnafu)?; self.labels .get_or_insert(Labels::new()) @@ -185,8 +188,8 @@ impl ObjectMetaBuilder { .ownerreference .as_ref() .map(|ownerreference| vec![ownerreference.clone()]), - labels: self.labels.clone().map(|l| l.into()), - annotations: self.annotations.clone().map(|a| a.into()), + labels: self.labels.as_ref().map(KeyValuePairs::to_unvalidated), + annotations: self.annotations.as_ref().map(KeyValuePairs::to_unvalidated), ..ObjectMeta::default() } } diff --git a/crates/stackable-operator/src/builder/pdb.rs b/crates/stackable-operator/src/builder/pdb.rs index 4232b2d78..cc2c25045 100644 --- a/crates/stackable-operator/src/builder/pdb.rs +++ b/crates/stackable-operator/src/builder/pdb.rs @@ -10,7 +10,7 @@ use snafu::{ResultExt, Snafu}; use crate::{ builder::meta::ObjectMetaBuilder, - kvp::{Label, Labels}, + kvp::{KeyValuePairsExt, label}, }; type Result = std::result::Result; @@ -85,10 +85,10 @@ impl PodDisruptionBudgetBuilder<(), (), ()> { operator_name: &str, controller_name: &str, ) -> Result> { - let role_selector_labels = - Labels::role_selector(owner, app_name, role).context(RoleSelectorLabelsSnafu)?; - let managed_by_label = - Label::managed_by(operator_name, controller_name).context(ManagedByLabelSnafu)?; + let role_selector_labels = label::well_known::sets::role_selector(owner, app_name, role) + .context(RoleSelectorLabelsSnafu)?; + let managed_by_label = label::well_known::managed_by(operator_name, controller_name) + .context(ManagedByLabelSnafu)?; let metadata = ObjectMetaBuilder::new() .namespace_opt(owner.namespace()) .name(format!("{}-{}", owner.name_any(), role)) @@ -102,7 +102,7 @@ impl PodDisruptionBudgetBuilder<(), (), ()> { metadata, selector: LabelSelector { match_expressions: None, - match_labels: Some(role_selector_labels.into()), + match_labels: Some(role_selector_labels.to_unvalidated()), }, ..PodDisruptionBudgetBuilder::default() }) diff --git a/crates/stackable-operator/src/builder/pod/mod.rs b/crates/stackable-operator/src/builder/pod/mod.rs index 7cb46f185..7fa52181c 100644 --- a/crates/stackable-operator/src/builder/pod/mod.rs +++ b/crates/stackable-operator/src/builder/pod/mod.rs @@ -330,16 +330,17 @@ impl PodBuilder { /// ``` /// # use stackable_operator::builder::pod::PodBuilder; /// # use stackable_operator::builder::pod::container::ContainerBuilder; + /// # use stackable_operator::iter::TryFromIterator; /// # use stackable_operator::kvp::Labels; /// # use k8s_openapi::{ /// # apimachinery::pkg::apis::meta::v1::ObjectMeta, /// # }; /// # use std::collections::BTreeMap; /// - /// let labels: Labels = Labels::try_from( - /// BTreeMap::from([("app.kubernetes.io/component", "test-role"), + /// let labels: Labels = Labels::try_from_iter( + /// [("app.kubernetes.io/component", "test-role"), /// ("app.kubernetes.io/instance", "test"), - /// ("app.kubernetes.io/name", "test")])) + /// ("app.kubernetes.io/name", "test")]) /// .unwrap(); /// /// let pod = PodBuilder::new() @@ -416,16 +417,17 @@ impl PodBuilder { /// ``` /// # use stackable_operator::builder::pod::PodBuilder; /// # use stackable_operator::builder::pod::container::ContainerBuilder; + /// # use stackable_operator::iter::TryFromIterator; /// # use stackable_operator::kvp::Labels; /// # use k8s_openapi::{ /// # apimachinery::pkg::apis::meta::v1::ObjectMeta, /// # }; /// # use std::collections::BTreeMap; /// - /// let labels: Labels = Labels::try_from( - /// BTreeMap::from([("app.kubernetes.io/component", "test-role"), + /// let labels: Labels = Labels::try_from_iter( + /// [("app.kubernetes.io/component", "test-role"), /// ("app.kubernetes.io/instance", "test"), - /// ("app.kubernetes.io/name", "test")])) + /// ("app.kubernetes.io/name", "test")]) /// .unwrap(); /// /// let pod = PodBuilder::new() diff --git a/crates/stackable-operator/src/builder/pod/volume.rs b/crates/stackable-operator/src/builder/pod/volume.rs index d27b54aa6..c982fb25c 100644 --- a/crates/stackable-operator/src/builder/pod/volume.rs +++ b/crates/stackable-operator/src/builder/pod/volume.rs @@ -13,7 +13,7 @@ use tracing::warn; use crate::{ builder::meta::ObjectMetaBuilder, - kvp::{Annotation, AnnotationError, Annotations, LabelError, Labels}, + kvp::{Annotation, AnnotationError, Annotations, LabelError, Labels, annotation}, time::Duration, }; @@ -340,24 +340,32 @@ impl SecretOperatorVolumeSourceBuilder { pub fn build(&self) -> Result { let mut annotations = Annotations::new(); - annotations - .insert(Annotation::secret_class(&self.secret_class).context(ParseAnnotationSnafu)?); + annotations.extend([annotation::well_known::secret_volume::secret_class( + &self.secret_class, + ) + .context(ParseAnnotationSnafu)?]); if !self.scopes.is_empty() { - annotations - .insert(Annotation::secret_scope(&self.scopes).context(ParseAnnotationSnafu)?); + annotations.extend([ + annotation::well_known::secret_volume::secret_scope(&self.scopes) + .context(ParseAnnotationSnafu)?, + ]); } if let Some(format) = &self.format { - annotations - .insert(Annotation::secret_format(format.as_ref()).context(ParseAnnotationSnafu)?); + annotations.extend([annotation::well_known::secret_volume::secret_format( + format.as_ref(), + ) + .context(ParseAnnotationSnafu)?]); } if !self.kerberos_service_names.is_empty() { - annotations.insert( - Annotation::kerberos_service_names(&self.kerberos_service_names) - .context(ParseAnnotationSnafu)?, - ); + annotations.extend([ + annotation::well_known::secret_volume::kerberos_service_names( + &self.kerberos_service_names, + ) + .context(ParseAnnotationSnafu)?, + ]); } if let Some(password) = &self.tls_pkcs12_password { @@ -365,17 +373,20 @@ impl SecretOperatorVolumeSourceBuilder { if Some(SecretFormat::TlsPkcs12) != self.format { warn!(format.actual = ?self.format, format.expected = ?Some(SecretFormat::TlsPkcs12), "A TLS PKCS12 password was set but ignored because another format was requested") } else { - annotations.insert( - Annotation::tls_pkcs12_password(password).context(ParseAnnotationSnafu)?, - ); + annotations.extend([annotation::well_known::secret_volume::tls_pkcs12_password( + password, + ) + .context(ParseAnnotationSnafu)?]); } } if let Some(lifetime) = &self.auto_tls_cert_lifetime { - annotations.insert( - Annotation::auto_tls_cert_lifetime(&lifetime.to_string()) - .context(ParseAnnotationSnafu)?, - ); + annotations.extend([ + annotation::well_known::secret_volume::auto_tls_cert_lifetime( + &lifetime.to_string(), + ) + .context(ParseAnnotationSnafu)?, + ]); } Ok(EphemeralVolumeSource { @@ -464,7 +475,7 @@ pub enum ListenerOperatorVolumeSourceBuilderError { /// # use std::collections::BTreeMap; /// let mut pod_builder = PodBuilder::new(); /// -/// let labels: Labels = Labels::try_from(BTreeMap::::new()).unwrap(); +/// let labels: Labels = Labels::new(); /// /// let volume_source = /// ListenerOperatorVolumeSourceBuilder::new( @@ -566,8 +577,6 @@ impl ListenerOperatorVolumeSourceBuilder { #[cfg(test)] mod tests { - use std::collections::BTreeMap; - use k8s_openapi::apimachinery::pkg::api::resource::Quantity; use super::*; @@ -630,7 +639,7 @@ mod tests { #[test] fn listener_operator_volume_source_builder() { - let labels: Labels = Labels::try_from(BTreeMap::::new()).unwrap(); + let labels: Labels = Labels::new(); let builder = ListenerOperatorVolumeSourceBuilder::new( &ListenerReference::ListenerClass("public".into()), diff --git a/crates/stackable-operator/src/cluster_resources.rs b/crates/stackable-operator/src/cluster_resources.rs index a7b0c03c1..40e8de04a 100644 --- a/crates/stackable-operator/src/cluster_resources.rs +++ b/crates/stackable-operator/src/cluster_resources.rs @@ -39,8 +39,9 @@ use crate::{ }, crd::listener, kvp::{ - Label, LabelError, Labels, + LabelError, Labels, consts::{K8S_APP_INSTANCE_KEY, K8S_APP_MANAGED_BY_KEY, K8S_APP_NAME_KEY}, + label, }, utils::format_full_controller_name, }; @@ -500,16 +501,17 @@ impl ClusterResources { /// Return required labels for cluster resources to be uniquely identified for clean up. // TODO: This is a (quick-fix) helper method but should be replaced by better label handling pub fn get_required_labels(&self) -> Result { - let mut labels = Labels::common(&self.app_name, &self.app_instance)?; + let mut labels = label::well_known::sets::common(&self.app_name, &self.app_instance)?; - labels.insert(Label::managed_by( + labels.extend([label::well_known::managed_by( &self.operator_name, &self.controller_name, - )?); + )?]); Ok(labels) } + // TODO (@Techassi): T should guarantee (by it's type) that required labels are set. /// Adds a resource to the cluster resources. /// /// The resource will be patched and the patched resource will be returned. diff --git a/crates/stackable-operator/src/commons/product_image_selection.rs b/crates/stackable-operator/src/commons/product_image_selection.rs index 6fe5ba674..40e419675 100644 --- a/crates/stackable-operator/src/commons/product_image_selection.rs +++ b/crates/stackable-operator/src/commons/product_image_selection.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use strum::AsRefStr; #[cfg(doc)] -use crate::kvp::Labels; +use crate::kvp::label; pub const STACKABLE_DOCKER_REPO: &str = "oci.stackable.tech/sdp"; @@ -67,7 +67,7 @@ pub struct ResolvedProductImage { /// Version of the product, e.g. `1.4.1`. pub product_version: String, - /// App version as formatted for [`Labels::recommended`] + /// App version as formatted for [`label::sets::recommended`] pub app_version_label: String, /// Image to be used for the product image e.g. `oci.stackable.tech/sdp/superset:1.4.1-stackable2.1.0` diff --git a/crates/stackable-operator/src/kvp/annotation/mod.rs b/crates/stackable-operator/src/kvp/annotation/mod.rs index c73d64c44..a77c04678 100644 --- a/crates/stackable-operator/src/kvp/annotation/mod.rs +++ b/crates/stackable-operator/src/kvp/annotation/mod.rs @@ -1,339 +1,135 @@ -//! This module provides various types and functions to construct valid Kubernetes -//! annotations. Annotations are key/value pairs, where the key must meet certain -//! requirementens regarding length and character set. The value can contain -//! **any** valid UTF-8 data. +//! This module provides various types and functions to construct valid Kubernetes annotations. +//! Annotations are key/value pairs, where the key must meet certain requirements regarding length +//! and character set. The value can contain **any** valid UTF-8 data. //! -//! Additionally, the [`Annotation`] struct provides various helper functions to -//! construct commonly used annotations across the Stackable Data Platform, like -//! the secret scope or class. +//! Additionally, the [`Annotation`] struct provides various helper functions to construct commonly +//! used annotations across the Stackable Data Platform, like the secret scope or class. //! -//! See -//! for more information on Kubernetes annotations. -use std::{ - collections::{BTreeMap, BTreeSet}, - convert::Infallible, - fmt::Display, -}; - -use delegate::delegate; +//! See [the documentation][1] for more information on Kubernetes annotations. +//! +//! [1]: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +use std::convert::Infallible; -use crate::{ - builder::pod::volume::SecretOperatorVolumeScope, - iter::TryFromIterator, - kvp::{Key, KeyValuePair, KeyValuePairError, KeyValuePairs, KeyValuePairsError}, -}; +use crate::kvp::{KeyValuePair, KeyValuePairError, KeyValuePairs}; mod value; pub use value::*; -pub type AnnotationsError = KeyValuePairsError; - -/// A type alias for errors returned when construction or manipulation of a set -/// of annotations fails. +/// A type alias for errors returned when construction or manipulation of a set of annotations fails. pub type AnnotationError = KeyValuePairError; -/// A specialized implementation of a key/value pair representing Kubernetes -/// annotations. +/// A specialized implementation of a key/value pair representing Kubernetes annotations. /// -/// The validation of the annotation value can **never** fail, as [`str`] is -/// guaranteed to only contain valid UTF-8 data - which is the only -/// requirement for a valid Kubernetes annotation value. +/// The validation of the annotation value can **never** fail, as [`str`] is guaranteed to only +/// contain valid UTF-8 data - which is the only requirement for a valid Kubernetes annotation value. /// -/// See -/// for more information on Kubernetes annotations. -#[derive(Debug)] -pub struct Annotation(KeyValuePair); - -impl TryFrom<(K, V)> for Annotation -where - K: AsRef, - V: AsRef, -{ - type Error = AnnotationError; - - fn try_from(value: (K, V)) -> Result { - let kvp = KeyValuePair::try_from(value)?; - Ok(Self(kvp)) - } -} - -impl Display for Annotation { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -impl Annotation { - /// Returns an immutable reference to the annotation's [`Key`]. - pub fn key(&self) -> &Key { - self.0.key() - } - - /// Returns an immutable reference to the annotation's value. - pub fn value(&self) -> &AnnotationValue { - self.0.value() - } - - /// Consumes self and returns the inner [`KeyValuePair`]. - pub fn into_inner(self) -> KeyValuePair { - self.0 - } - - /// Constructs a `secrets.stackable.tech/class` annotation. - pub fn secret_class(secret_class: &str) -> Result { - let kvp = KeyValuePair::try_from(("secrets.stackable.tech/class", secret_class))?; - Ok(Self(kvp)) - } - - /// Constructs a `secrets.stackable.tech/scope` annotation. - pub fn secret_scope( - scopes: impl AsRef<[SecretOperatorVolumeScope]>, - ) -> Result { - let mut value = String::new(); - - for scope in scopes.as_ref() { - if !value.is_empty() { - value.push(','); - } - - match scope { - SecretOperatorVolumeScope::Node => value.push_str("node"), - SecretOperatorVolumeScope::Pod => value.push_str("pod"), - SecretOperatorVolumeScope::Service { name } => { - value.push_str("service="); - value.push_str(name); - } - SecretOperatorVolumeScope::ListenerVolume { name } => { - value.push_str("listener-volume="); - value.push_str(name); - } - } - } - - let kvp = KeyValuePair::try_from(("secrets.stackable.tech/scope", value))?; - Ok(Self(kvp)) - } - - /// Constructs a `secrets.stackable.tech/format` annotation. - pub fn secret_format(format: &str) -> Result { - let kvp = KeyValuePair::try_from(("secrets.stackable.tech/format", format))?; - Ok(Self(kvp)) - } - - /// Constructs a `secrets.stackable.tech/kerberos.service.names` annotation. - pub fn kerberos_service_names(names: impl AsRef<[String]>) -> Result { - let names = names.as_ref().join(","); - let kvp = KeyValuePair::try_from(("secrets.stackable.tech/kerberos.service.names", names))?; - Ok(Self(kvp)) - } - - /// Constructs a `secrets.stackable.tech/format.compatibility.tls-pkcs12.password` - /// annotation. - pub fn tls_pkcs12_password(password: &str) -> Result { - let kvp = KeyValuePair::try_from(( - "secrets.stackable.tech/format.compatibility.tls-pkcs12.password", - password, - ))?; - Ok(Self(kvp)) - } - - /// Constructs a `secrets.stackable.tech/backend.autotls.cert.lifetime` annotation. - pub fn auto_tls_cert_lifetime(lifetime: &str) -> Result { - let kvp = KeyValuePair::try_from(( - "secrets.stackable.tech/backend.autotls.cert.lifetime", - lifetime, - ))?; - Ok(Self(kvp)) - } -} +/// See [the documentation][1] for more information on Kubernetes annotations. +/// +/// [1]: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +pub type Annotation = KeyValuePair; /// A validated set/list of Kubernetes annotations. /// -/// It provides selected associated functions to manipulate the set of -/// annotations, like inserting or extending. -/// /// ## Examples /// /// ### Converting a BTreeMap into a list of labels /// /// ``` /// # use std::collections::BTreeMap; +/// # use stackable_operator::iter::TryFromIterator; /// # use stackable_operator::kvp::Annotations; /// let map = BTreeMap::from([ /// ("stackable.tech/managed-by", "stackablectl"), /// ("stackable.tech/vendor", "Stäckable"), /// ]); /// -/// let labels = Annotations::try_from(map).unwrap(); +/// let labels = Annotations::try_from_iter(map).unwrap(); /// ``` /// /// ### Creating a list of labels from an array /// /// ``` +/// # use stackable_operator::iter::TryFromIterator; /// # use stackable_operator::kvp::Annotations; -/// let labels = Annotations::try_from([ +/// let labels = Annotations::try_from_iter([ /// ("stackable.tech/managed-by", "stackablectl"), /// ("stackable.tech/vendor", "Stäckable"), /// ]).unwrap(); /// ``` -#[derive(Clone, Debug, Default)] -pub struct Annotations(KeyValuePairs); - -impl TryFrom> for Annotations -where - K: AsRef, - V: AsRef, -{ - type Error = AnnotationError; - - fn try_from(map: BTreeMap) -> Result { - Self::try_from_iter(map) - } -} - -impl TryFrom<&BTreeMap> for Annotations -where - K: AsRef, - V: AsRef, -{ - type Error = AnnotationError; - - fn try_from(map: &BTreeMap) -> Result { - Self::try_from_iter(map) - } -} - -impl TryFrom<[(K, V); N]> for Annotations -where - K: AsRef, - V: AsRef, -{ - type Error = AnnotationError; - - fn try_from(array: [(K, V); N]) -> Result { - Self::try_from_iter(array) - } -} - -impl FromIterator> for Annotations { - fn from_iter>>(iter: T) -> Self { - let kvps = KeyValuePairs::from_iter(iter); - Self(kvps) - } -} - -impl TryFromIterator<(K, V)> for Annotations -where - K: AsRef, - V: AsRef, -{ - type Error = AnnotationError; - - fn try_from_iter>(iter: I) -> Result { - let kvps = KeyValuePairs::try_from_iter(iter)?; - Ok(Self(kvps)) - } -} - -impl From for BTreeMap { - fn from(value: Annotations) -> Self { - value.0.into() - } -} - -impl Annotations { - // This forwards / delegates associated functions to the inner field. In - // this case self.0 which is of type KeyValuePairs. So calling - // Annotations::len() will be delegated to KeyValuePair::len() without - // the need to write boilerplate code. - delegate! { - to self.0 { - /// Tries to insert a new [`Annotation`]. It ensures there are no duplicate - /// entries. Trying to insert duplicated data returns an error. If no such - /// check is required, use [`Annotations::insert`] instead. - pub fn try_insert(&mut self, #[newtype] annotation: Annotation) -> Result<(), AnnotationsError>; - - /// Extends `self` with `other`. - pub fn extend(&mut self, #[newtype] other: Self); - - /// Returns the number of labels. - pub fn len(&self) -> usize; - - /// Returns if the set of labels is empty. - pub fn is_empty(&self) -> bool; - - /// Returns if the set of annotations contains the provided - /// `annotation`. Failure to parse/validate the [`KeyValuePair`] - /// will return `false`. - pub fn contains(&self, annotation: impl TryInto>) -> bool; - - /// Returns if the set of annotations contains a label with the - /// provided `key`. Failure to parse/validate the [`Key`] will - /// return `false`. - pub fn contains_key(&self, key: impl TryInto) -> bool; - - /// Returns an [`Iterator`] over [`Annotations`] yielding a reference to every [`Annotation`] contained within. - pub fn iter(&self) -> impl Iterator> + '_; +pub type Annotations = KeyValuePairs; + +/// Well-known annotations used by other tools or standard conventions. +pub mod well_known { + /// Annotations applicable to Stackable Secret Operator volumes + pub mod secret_volume { + use crate::{ + builder::pod::volume::SecretOperatorVolumeScope, + kvp::{Annotation, AnnotationError}, + }; + + /// Constructs a `secrets.stackable.tech/class` annotation. + pub fn secret_class(secret_class: &str) -> Result { + Annotation::try_from(("secrets.stackable.tech/class", secret_class)) } - } - /// Creates a new empty list of [`Annotations`]. - pub fn new() -> Self { - Self::default() - } - - /// Creates a new list of [`Annotations`] from `pairs`. - pub fn new_with(pairs: BTreeSet>) -> Self { - Self(KeyValuePairs::new_with(pairs)) - } - - /// Tries to insert a new annotation by first parsing `annotation` as an - /// [`Annotation`] and then inserting it into the list. This function will - /// overwrite any existing annotation already present. - pub fn parse_insert( - &mut self, - annotation: impl TryInto, - ) -> Result<(), AnnotationError> { - self.0.insert(annotation.try_into()?.0); - Ok(()) - } - - /// Inserts a new [`Annotation`]. This function will overwrite any existing - /// annotation already present. - pub fn insert(&mut self, annotation: Annotation) -> &mut Self { - self.0.insert(annotation.0); - self - } -} + /// Constructs a `secrets.stackable.tech/scope` annotation. + pub fn secret_scope( + scopes: &[SecretOperatorVolumeScope], + ) -> Result { + let mut value = String::new(); -impl IntoIterator for Annotations { - type IntoIter = as IntoIterator>::IntoIter; - type Item = KeyValuePair; + for scope in scopes { + if !value.is_empty() { + value.push(','); + } - /// Returns a consuming [`Iterator`] over [`Annotations`] moving every [`Annotation`] out. - /// The [`Annotations`] cannot be used again after calling this. - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} + match scope { + SecretOperatorVolumeScope::Node => value.push_str("node"), + SecretOperatorVolumeScope::Pod => value.push_str("pod"), + SecretOperatorVolumeScope::Service { name } => { + value.push_str("service="); + value.push_str(name); + } + SecretOperatorVolumeScope::ListenerVolume { name } => { + value.push_str("listener-volume="); + value.push_str(name); + } + } + } -#[cfg(test)] -mod test { - use super::*; + Annotation::try_from(("secrets.stackable.tech/scope", value.as_str())) + } - #[test] - fn parse_insert() { - let mut annotations = Annotations::new(); + /// Constructs a `secrets.stackable.tech/format` annotation. + pub fn secret_format(format: &str) -> Result { + Annotation::try_from(("secrets.stackable.tech/format", format)) + } - annotations - .parse_insert(("stackable.tech/managed-by", "stackablectl")) - .unwrap(); + /// Constructs a `secrets.stackable.tech/kerberos.service.names` annotation. + pub fn kerberos_service_names(names: &[String]) -> Result { + let names = names.join(","); + Annotation::try_from(( + "secrets.stackable.tech/kerberos.service.names", + names.as_str(), + )) + } - annotations - .parse_insert(("stackable.tech/vendor", "Stäckable")) - .unwrap(); + /// Constructs a `secrets.stackable.tech/format.compatibility.tls-pkcs12.password` + /// annotation. + pub fn tls_pkcs12_password(password: &str) -> Result { + Annotation::try_from(( + "secrets.stackable.tech/format.compatibility.tls-pkcs12.password", + password, + )) + } - assert_eq!(annotations.len(), 2); + /// Constructs a `secrets.stackable.tech/backend.autotls.cert.lifetime` annotation. + pub fn auto_tls_cert_lifetime(lifetime: &str) -> Result { + Annotation::try_from(( + "secrets.stackable.tech/backend.autotls.cert.lifetime", + lifetime, + )) + } } } diff --git a/crates/stackable-operator/src/kvp/key.rs b/crates/stackable-operator/src/kvp/key.rs index 98ef7cb1d..4495b35c4 100644 --- a/crates/stackable-operator/src/kvp/key.rs +++ b/crates/stackable-operator/src/kvp/key.rs @@ -109,6 +109,13 @@ impl Display for Key { } } +// Allows SNAFU context selectors to clone the key implicitly +impl From<&Key> for Key { + fn from(value: &Key) -> Self { + value.clone() + } +} + impl Key { /// Retrieves the key's prefix. /// @@ -416,7 +423,7 @@ mod test { let label = Label::try_from((key, "zookeeper")).unwrap(); let is_valid = label - .key() + .key .prefix() .is_some_and(|prefix| *prefix == "app.kubernetes.io"); @@ -430,7 +437,7 @@ mod test { #[case("foo", false)] fn key_name_deref(#[case] key: &str, #[case] expected: bool) { let label = Label::try_from((key, "zookeeper")).unwrap(); - let is_valid = *label.key().name() == "name"; + let is_valid = *label.key.name() == "name"; assert_eq!(is_valid, expected); } diff --git a/crates/stackable-operator/src/kvp/label/mod.rs b/crates/stackable-operator/src/kvp/label/mod.rs index 15411a198..f06d7fffd 100644 --- a/crates/stackable-operator/src/kvp/label/mod.rs +++ b/crates/stackable-operator/src/kvp/label/mod.rs @@ -1,34 +1,14 @@ -//! This module provides various types and functions to construct valid -//! Kubernetes labels. Labels are key/value pairs, where the key must meet -//! certain requirementens regarding length and character set. The value can -//! contain a limited set of ASCII characters. +//! This module provides various types and functions to construct valid Kubernetes labels. Labels +//! are key/value pairs, where the key must meet certain requirements regarding length and character +//! set. The value can contain a limited set of ASCII characters. //! -//! Additionally, the [`Label`] struct provides various helper functions to -//! construct commonly used labels across the Stackable Data Platform, like -//! the role_group or component. +//! Additionally, the [`Label`] struct provides various helper functions to construct commonly used +//! labels across the Stackable Data Platform, like the `role_group` or `component`. //! -//! See -//! for more information on Kubernetes labels. -use std::{ - collections::{BTreeMap, BTreeSet}, - fmt::Display, -}; +//! See for more +//! information on Kubernetes labels. -use delegate::delegate; -use kube::{Resource, ResourceExt}; - -use crate::{ - iter::TryFromIterator, - kvp::{ - Key, KeyValuePair, KeyValuePairError, KeyValuePairs, KeyValuePairsError, ObjectLabels, - consts::{ - K8S_APP_COMPONENT_KEY, K8S_APP_INSTANCE_KEY, K8S_APP_MANAGED_BY_KEY, K8S_APP_NAME_KEY, - K8S_APP_ROLE_GROUP_KEY, K8S_APP_VERSION_KEY, STACKABLE_VENDOR_KEY, - STACKABLE_VENDOR_VALUE, - }, - }, - utils::format_full_controller_name, -}; +use crate::kvp::{KeyValuePair, KeyValuePairError, KeyValuePairs}; mod selector; mod value; @@ -36,14 +16,10 @@ mod value; pub use selector::*; pub use value::*; -pub type LabelsError = KeyValuePairsError; - -/// A type alias for errors returned when construction or manipulation of a set -/// of labels fails. +/// A type alias for errors returned when construction or manipulation of a set of labels fails. pub type LabelError = KeyValuePairError; -/// A specialized implementation of a key/value pair representing Kubernetes -/// labels. +/// A specialized implementation of a key/value pair representing Kubernetes labels. /// /// ``` /// # use stackable_operator::kvp::Label; @@ -51,373 +27,194 @@ pub type LabelError = KeyValuePairError; /// assert_eq!(label.to_string(), "stackable.tech/vendor=Stackable"); /// ``` /// -/// The validation of the label value can fail due to multiple reasons. It can -/// only contain a limited set and combination of ASCII characters. See -/// -/// for more information on Kubernetes labels. -#[derive(Clone, Debug)] -pub struct Label(KeyValuePair); - -impl TryFrom<(K, V)> for Label -where - K: AsRef, - V: AsRef, -{ - type Error = LabelError; - - fn try_from(value: (K, V)) -> Result { - let kvp = KeyValuePair::try_from(value)?; - Ok(Self(kvp)) - } -} - -impl Display for Label { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -impl Label { - /// Returns an immutable reference to the label's [`Key`]. - /// - /// ``` - /// # use stackable_operator::kvp::Label; - /// let label = Label::try_from(("stackable.tech/vendor", "Stackable")).unwrap(); - /// assert_eq!(label.key().to_string(), "stackable.tech/vendor"); - /// ``` - pub fn key(&self) -> &Key { - self.0.key() - } - - /// Returns an immutable reference to the label's value. - pub fn value(&self) -> &LabelValue { - self.0.value() - } - - /// Consumes self and returns the inner [`KeyValuePair`]. - pub fn into_inner(self) -> KeyValuePair { - self.0 - } - - /// Creates the `app.kubernetes.io/component` label with `role` as the - /// value. This function will return an error if `role` violates the required - /// Kubernetes restrictions. - pub fn component(component: &str) -> Result { - let kvp = KeyValuePair::try_from((K8S_APP_COMPONENT_KEY, component))?; - Ok(Self(kvp)) - } - - /// Creates the `app.kubernetes.io/role-group` label with `role_group` as - /// the value. This function will return an error if `role_group` violates - /// the required Kubernetes restrictions. - pub fn role_group(role_group: &str) -> Result { - let kvp = KeyValuePair::try_from((K8S_APP_ROLE_GROUP_KEY, role_group))?; - Ok(Self(kvp)) - } - - /// Creates the `app.kubernetes.io/managed-by` label with the formated - /// full controller name based on `operator_name` and `controller_name` as - /// the value. This function will return an error if the formatted controller - /// name violates the required Kubernetes restrictions. - pub fn managed_by(operator_name: &str, controller_name: &str) -> Result { - let kvp = KeyValuePair::try_from(( - K8S_APP_MANAGED_BY_KEY, - format_full_controller_name(operator_name, controller_name).as_str(), - ))?; - Ok(Self(kvp)) - } - - /// Creates the `app.kubernetes.io/version` label with `version` as the - /// value. This function will return an error if `role_group` violates the - /// required Kubernetes restrictions. - pub fn version(version: &str) -> Result { - // NOTE (Techassi): Maybe use semver::Version - let kvp = KeyValuePair::try_from((K8S_APP_VERSION_KEY, version))?; - Ok(Self(kvp)) - } -} +/// The validation of the label value can fail due to multiple reasons. It can only contain a +/// limited set and combination of ASCII characters. See [the documentation][1] for more information +/// on Kubernetes labels. +/// +/// [1]: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +pub type Label = KeyValuePair; /// A validated set/list of Kubernetes labels. /// -/// It provides selected associated functions to manipulate the set of labels, -/// like inserting or extending. -/// /// ## Examples /// /// ### Converting a BTreeMap into a list of labels /// /// ``` /// # use std::collections::BTreeMap; +/// # use stackable_operator::iter::TryFromIterator; /// # use stackable_operator::kvp::Labels; /// let map = BTreeMap::from([ /// ("stackable.tech/managed-by", "stackablectl"), /// ("stackable.tech/vendor", "Stackable"), /// ]); /// -/// let labels = Labels::try_from(map).unwrap(); +/// let labels = Labels::try_from_iter(map).unwrap(); /// ``` /// /// ### Creating a list of labels from an array /// /// ``` +/// # use stackable_operator::iter::TryFromIterator; /// # use stackable_operator::kvp::Labels; -/// let labels = Labels::try_from([ +/// let labels = Labels::try_from_iter([ /// ("stackable.tech/managed-by", "stackablectl"), /// ("stackable.tech/vendor", "Stackable"), /// ]).unwrap(); /// ``` -#[derive(Clone, Debug, Default)] -pub struct Labels(KeyValuePairs); - -impl TryFrom> for Labels -where - K: AsRef, - V: AsRef, -{ - type Error = LabelError; - - fn try_from(map: BTreeMap) -> Result { - Self::try_from_iter(map) - } -} - -impl TryFrom<&BTreeMap> for Labels -where - K: AsRef, - V: AsRef, -{ - type Error = LabelError; - - fn try_from(map: &BTreeMap) -> Result { - Self::try_from_iter(map) - } -} - -impl TryFrom<[(K, V); N]> for Labels -where - K: AsRef, - V: AsRef, -{ - type Error = LabelError; - - fn try_from(array: [(K, V); N]) -> Result { - Self::try_from_iter(array) - } -} - -impl FromIterator> for Labels { - fn from_iter>>(iter: T) -> Self { - let kvps = KeyValuePairs::from_iter(iter); - Self(kvps) - } -} - -impl TryFromIterator<(K, V)> for Labels -where - K: AsRef, - V: AsRef, -{ - type Error = LabelError; +pub type Labels = KeyValuePairs; - fn try_from_iter>(iter: I) -> Result { - let kvps = KeyValuePairs::try_from_iter(iter)?; - Ok(Self(kvps)) - } -} - -impl From for BTreeMap { - fn from(value: Labels) -> Self { - value.0.into() - } -} - -impl Labels { - // This forwards / delegates associated functions to the inner field. In - // this case self.0 which is of type KeyValuePairs. So calling - // Labels::len() will be delegated to KeyValuePair::len() without the - // need to write boilerplate code. - delegate! { - to self.0 { - /// Tries to insert a new [`Label`]. It ensures there are no duplicate - /// entries. Trying to insert duplicated data returns an error. If no such - /// check is required, use [`Labels::insert`] instead. - pub fn try_insert(&mut self, #[newtype] label: Label) -> Result<(), LabelsError>; - - /// Extends `self` with `other`. - pub fn extend(&mut self, #[newtype] other: Self); - - /// Returns the number of labels. - pub fn len(&self) -> usize; - - /// Returns if the set of labels is empty. - pub fn is_empty(&self) -> bool; - - /// Returns if the set of labels contains the provided `label`. Failure to - /// parse/validate the [`KeyValuePair`] will return `false`. - pub fn contains(&self, label: impl TryInto>) -> bool; - - /// Returns if the set of labels contains a label with the provided `key`. - /// Failure to parse/validate the [`Key`] will return `false`. - pub fn contains_key(&self, key: impl TryInto) -> bool; - - /// Returns an [`Iterator`] over [`Labels`] yielding a reference to every [`Label`] contained within. - pub fn iter(&self) -> impl Iterator> + '_; - - } - } - - /// Creates a new empty list of [`Labels`]. - pub fn new() -> Self { - Self::default() - } - - /// Creates a new list of [`Labels`] from `pairs`. - pub fn new_with(pairs: BTreeSet>) -> Self { - Self(KeyValuePairs::new_with(pairs)) - } - - /// Tries to insert a new label by first parsing `label` as a [`Label`] - /// and then inserting it into the list. This function will overwrite any - /// existing label already present. - pub fn parse_insert( - &mut self, - label: impl TryInto, - ) -> Result<(), LabelError> { - self.0.insert(label.try_into()?.0); - Ok(()) - } - - /// Inserts a new [`Label`]. This function will overwrite any existing label - /// already present. - pub fn insert(&mut self, label: Label) -> &mut Self { - self.0.insert(label.0); - self - } +/// Well-known labels used by other tools or standard conventions. +pub mod well_known { + use super::{Label, LabelError, Labels}; + use crate::utils::format_full_controller_name; - /// Returns the recommended set of labels. The set includes these well-known - /// Kubernetes labels: + /// Creates the `app.kubernetes.io/component` label. /// - /// - `app.kubernetes.io/role-group` - /// - `app.kubernetes.io/managed-by` - /// - `app.kubernetes.io/component` - /// - `app.kubernetes.io/instance` - /// - `app.kubernetes.io/version` - /// - `app.kubernetes.io/name` + /// It is used to specify the component within the architecture, e.g. `database`. /// - /// Additionally, it includes Stackable-specific labels. These are: - /// - /// - `stackable.tech/vendor` - /// - /// This function returns a result, because the parameter `object_labels` - /// can contain invalid data or can exceed the maximum allowed number of - /// characters. - pub fn recommended(object_labels: ObjectLabels) -> Result - where - R: Resource, - { - // Well-known Kubernetes labels - let mut labels = Self::role_group_selector( - object_labels.owner, - object_labels.app_name, - object_labels.role, - object_labels.role_group, - )?; - - let managed_by = - Label::managed_by(object_labels.operator_name, object_labels.controller_name)?; - let version = Label::version(object_labels.app_version)?; - - labels.insert(managed_by); - labels.insert(version); - - // Stackable-specific labels - labels.parse_insert((STACKABLE_VENDOR_KEY, STACKABLE_VENDOR_VALUE))?; - - Ok(labels) + /// This function will return an error if `role` violates the required Kubernetes restrictions. + pub fn component(component: &str) -> Result { + Label::try_from(("app.kubernetes.io/component", component)) } - /// Returns the set of labels required to select the resource based on the - /// role group. The set contains role selector labels, see - /// [`Labels::role_selector`] for more details. Additionally, it contains - /// the `app.kubernetes.io/role-group` label with `role_group` as the value. - pub fn role_group_selector( - owner: &R, - app_name: &str, - role: &str, - role_group: &str, - ) -> Result - where - R: Resource, - { - let mut labels = Self::role_selector(owner, app_name, role)?; - labels.insert(Label::role_group(role_group)?); - Ok(labels) - } - - /// Returns the set of labels required to select the resource based on the - /// role. The set contains the common labels, see [`Labels::common`] for - /// more details. Additionally, it contains the `app.kubernetes.io/component` - /// label with `role` as the value. + /// Creates the `app.kubernetes.io/managed-by` label with the formatted full controller name as + /// a value. + /// + /// The controller name is based on `operator_name` and `controller_name`. It is used to + /// indicate what tool is being used to manage the operation of an application, e.g. `helm`. /// - /// This function returns a result, because the parameters `owner`, `app_name`, - /// and `role` can contain invalid data or can exceed the maximum allowed - /// number fo characters. - pub fn role_selector(owner: &R, app_name: &str, role: &str) -> Result - where - R: Resource, - { - let mut labels = Self::common(app_name, owner.name_any().as_str())?; - labels.insert(Label::component(role)?); - Ok(labels) + /// This function will return an error if the formatted controller name violates the required + /// Kubernetes restrictions. + pub fn managed_by(operator_name: &str, controller_name: &str) -> Result { + Label::try_from(( + "app.kubernetes.io/managed-by", + format_full_controller_name(operator_name, controller_name).as_str(), + )) } - /// Returns a common set of labels, which are required to identify resources - /// that belong to a certain owner object, for example a `ZookeeperCluster`. - /// The set contains these well-known labels: + /// Creates the `app.kubernetes.io/version` label. /// - /// - `app.kubernetes.io/instance` and - /// - `app.kubernetes.io/name` + /// It is used to indicate the current version of the application. The value can represent a + /// semantic version or a revision, e.g. `5.7.21`. /// - /// This function returns a result, because the parameters `app_name` and - /// `app_instance` can contain invalid data or can exceed the maximum - /// allowed number of characters. - pub fn common(app_name: &str, app_instance: &str) -> Result { - let mut labels = Self::new(); - - labels.insert((K8S_APP_INSTANCE_KEY, app_instance).try_into()?); - labels.insert((K8S_APP_NAME_KEY, app_name).try_into()?); - - Ok(labels) + /// This function will return an error if `role_group` violates the required Kubernetes + /// restrictions. + pub fn version(version: &str) -> Result { + Label::try_from(("app.kubernetes.io/version", version)) } -} -impl IntoIterator for Labels { - type IntoIter = as IntoIterator>::IntoIter; - type Item = KeyValuePair; - - /// Returns a consuming [`Iterator`] over [`Labels`] moving every [`Label`] out. - /// The [`Labels`] cannot be used again after calling this. - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() + /// Creates the `stackable.tech/vendor: Stackable` label, tagging the object as created by a + /// Stackable operator. + pub fn vendor_stackable() -> Label { + Label::try_from(("stackable.tech/vendor", "Stackable")) + .expect("failed to parse hard-coded Stackable vendor label") } -} - -#[cfg(test)] -mod test { - use super::*; - #[test] - fn parse_insert() { - let mut labels = Labels::new(); + /// Creates the `stackable.tech/role-group` label. + /// + /// This function will return an error if `role_group` violates the required Kubernetes + /// restrictions. + pub fn role_group(role_group: &str) -> Result { + Label::try_from(("stackable.tech/role-group", role_group)) + } + + /// Common sets of labels that apply for different use-cases. + pub mod sets { + use kube::{Resource, ResourceExt}; + + use super::{Label, LabelError, Labels}; + use crate::kvp::ObjectLabels; + + /// Returns the recommended set of labels. + /// + /// The set includes these well-known Kubernetes labels: + /// + /// - `app.kubernetes.io/managed-by` + /// - `app.kubernetes.io/component` + /// - `app.kubernetes.io/instance` + /// - `app.kubernetes.io/version` + /// - `app.kubernetes.io/name` + /// + /// Additionally, it includes these Stackable-specific labels: + /// + /// - `stackable.tech/role-group` + /// - `stackable.tech/vendor` + /// + /// This function returns a [`Result`], because the parameter `object_labels` can contain + /// invalid data or can exceed the maximum allowed number of characters. + pub fn recommended(object_labels: ObjectLabels) -> Result + where + R: Resource, + { + let mut labels = role_group_selector( + object_labels.owner, + object_labels.app_name, + object_labels.role, + object_labels.role_group, + )?; + + labels.extend([ + super::managed_by(object_labels.operator_name, object_labels.controller_name)?, + super::version(object_labels.app_version)?, + // Stackable-specific labels + super::vendor_stackable(), + ]); + + Ok(labels) + } - labels - .parse_insert(("stackable.tech/managed-by", "stackablectl")) - .unwrap(); + /// Returns the set of labels required to select the resource based on the role group. + /// + /// The set contains role selector labels, see [`role_selector`] for more details. + /// Additionally, it contains the `stackable.tech/role-group` label with `role_group` as the + /// value. + pub fn role_group_selector( + owner: &R, + app_name: &str, + role: &str, + role_group: &str, + ) -> Result + where + R: Resource, + { + let mut labels = role_selector(owner, app_name, role)?; + labels.extend([super::role_group(role_group)?]); + Ok(labels) + } - labels - .parse_insert(("stackable.tech/vendor", "Stackable")) - .unwrap(); + /// Returns the set of labels required to select the resource based on the role. + /// + /// The set contains the common labels, see [`common`] for more details. Additionally, it + /// contains the `app.kubernetes.io/component` label with `role` as the value. + /// + /// This function returns a result, because the parameters `owner`, `app_name`, + /// and `role` can contain invalid data or can exceed the maximum allowed + /// number fo characters. + pub fn role_selector(owner: &R, app_name: &str, role: &str) -> Result + where + R: Resource, + { + let mut labels = common(app_name, owner.name_any().as_str())?; + labels.extend([super::component(role)?]); + Ok(labels) + } - assert_eq!(labels.len(), 2); + /// Returns a common set of labels, which are required to identify resources that belong to + /// a certain owner object, for example a `ZookeeperCluster`. + /// + /// The set contains these well-known labels: + /// + /// - `app.kubernetes.io/instance` and + /// - `app.kubernetes.io/name` + /// + /// This function returns a result, because the parameters `app_name` and `app_instance` can + /// contain invalid data or can exceed the maximum allowed number of characters. + pub fn common(app_name: &str, app_instance: &str) -> Result { + Ok(Labels::from_iter([ + Label::try_from(("app.kubernetes.io/instance", app_instance))?, + Label::try_from(("app.kubernetes.io/name", app_name))?, + ])) + } } } diff --git a/crates/stackable-operator/src/kvp/mod.rs b/crates/stackable-operator/src/kvp/mod.rs index f5aab1b83..7965aa691 100644 --- a/crates/stackable-operator/src/kvp/mod.rs +++ b/crates/stackable-operator/src/kvp/mod.rs @@ -1,25 +1,30 @@ //! Utility functions and data structures the create and manage Kubernetes //! key/value pairs, like labels and annotations. use std::{ - collections::{BTreeMap, BTreeSet}, - fmt::Display, - ops::Deref, + collections::BTreeMap, + fmt::{Debug, Display}, str::FromStr, }; -use snafu::{ResultExt, Snafu, ensure}; +use snafu::{ResultExt, Snafu}; use crate::iter::TryFromIterator; -mod annotation; +pub mod annotation; pub mod consts; +pub mod label; + mod key; -mod label; mod value; -pub use annotation::*; +#[cfg(doc)] +use std::ops::Deref; + +pub use annotation::{Annotation, AnnotationError, AnnotationValue, Annotations}; +#[cfg(doc)] +use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; pub use key::*; -pub use label::*; +pub use label::{Label, LabelError, LabelSelectorExt, LabelValue, Labels, SelectorError}; pub use value::*; /// The error type for key/value pair parsing/validating operations. @@ -34,12 +39,8 @@ where InvalidKey { source: KeyError, key: String }, /// Indicates that the value failed to parse. - #[snafu(display("failed to parse value {value:?} of key {key:?}"))] - InvalidValue { - source: E, - key: String, - value: String, - }, + #[snafu(display("failed to parse value {value:?} for key {key:?}", key = key.to_string()))] + InvalidValue { source: E, key: Key, value: String }, } /// A validated Kubernetes key/value pair. @@ -90,268 +91,102 @@ where /// /// - /// - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct KeyValuePair +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct KeyValuePair where - T: Value, + V: Value, { - key: Key, - value: T, + pub key: Key, + pub value: V, } -impl TryFrom<(K, V)> for KeyValuePair +impl TryFrom<(&str, &str)> for KeyValuePair where - K: AsRef, - V: AsRef, - T: Value, + V: Value, { - type Error = KeyValuePairError; - - fn try_from(value: (K, V)) -> Result { - let key = Key::from_str(value.0.as_ref()).context(InvalidKeySnafu { - key: value.0.as_ref(), - })?; - - let value = T::from_str(value.1.as_ref()).context(InvalidValueSnafu { - key: key.to_string(), - value: value.1.as_ref(), - })?; + type Error = KeyValuePairError; + fn try_from((key, value): (&str, &str)) -> Result { + let key = Key::from_str(key).context(InvalidKeySnafu { key })?; + let value = V::from_str(value).context(InvalidValueSnafu { key: &key, value })?; Ok(Self { key, value }) } } -impl Display for KeyValuePair -where - T: Value, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}={}", self.key, self.value) +impl From> for (Key, V) { + fn from(KeyValuePair { key, value }: KeyValuePair) -> Self { + (key, value) } } -impl KeyValuePair -where - T: Value, -{ - /// Creates a new [`KeyValuePair`] from a validated [`Key`] and value. - pub fn new(key: Key, value: T) -> Self { - Self { key, value } - } - - /// Returns an immutable reference to the pair's [`Key`]. - pub fn key(&self) -> &Key { - &self.key - } - - /// Returns an immutable reference to the pair's value. - pub fn value(&self) -> &T { - &self.value +impl Display for KeyValuePair { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}={}", self.key, self.value) } } -#[derive(Debug, PartialEq, Snafu)] -pub enum KeyValuePairsError { - #[snafu(display("key already exists"))] - KeyAlreadyExists, +impl Debug for KeyValuePair { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}: {:?}", self.key, self.value) + } } /// A validated set/list of Kubernetes key/value pairs. /// -/// It implements various traits which allows conversion from and to different -/// data types. Traits to construct [`KeyValuePairs`] from other data types are: +/// See [`Annotations`] and [`Labels`] for actual instantiations. /// -/// - `TryFrom<&BTreeMap>` -/// - `TryFrom>` -/// - `FromIterator>` -/// - `TryFrom<[(K, V); N]>` -/// -/// Traits to convert [`KeyValuePairs`] into a different data type are: -/// -/// - `From> for BTreeMap` -/// -/// See [`Labels`] and [`Annotations`] on how these traits can be used. -/// -/// # Note -/// -/// A [`BTreeSet`] is used as the inner collection to preserve order of items -/// which ultimately prevent unncessary reconciliations due to changes -/// in item order. -#[derive(Clone, Debug, Default)] -pub struct KeyValuePairs(BTreeMap); - -impl TryFrom> for KeyValuePairs -where - K: AsRef, - V: AsRef, - T: Value, -{ - type Error = KeyValuePairError; +/// See [`KeyValuePairsExt`] for kvp-specific convenience helpers. +pub type KeyValuePairs = BTreeMap; - fn try_from(map: BTreeMap) -> Result { - Self::try_from_iter(map) +impl Extend> for KeyValuePairs { + fn extend>>(&mut self, iter: T) { + self.extend(iter.into_iter().map(<(Key, V)>::from)); } } -impl TryFrom<&BTreeMap> for KeyValuePairs -where - K: AsRef, - V: AsRef, - T: Value, -{ - type Error = KeyValuePairError; - - fn try_from(map: &BTreeMap) -> Result { - Self::try_from_iter(map) +impl FromIterator> for KeyValuePairs { + fn from_iter>>(iter: T) -> Self { + Self::from_iter(iter.into_iter().map(<(Key, V)>::from)) } } -impl TryFrom<[(K, V); N]> for KeyValuePairs -where - K: AsRef, - V: AsRef, - T: Value + std::default::Default, -{ - type Error = KeyValuePairError; +/// Helpers for [`KeyValuePairs`]. +pub trait KeyValuePairsExt { + /// Clones `self` into a type without validation types, ready for use in [`ObjectMeta::annotations`]/[`ObjectMeta::labels`]. + fn to_unvalidated(&self) -> BTreeMap; - fn try_from(array: [(K, V); N]) -> Result { - Self::try_from_iter(array) - } -} - -impl FromIterator> for KeyValuePairs -where - T: Value, -{ - fn from_iter>>(iter: I) -> Self { - Self(iter.into_iter().map(|kvp| (kvp.key, kvp.value)).collect()) - } -} - -impl TryFromIterator<(K, V)> for KeyValuePairs -where - K: AsRef, - V: AsRef, - T: Value, -{ - type Error = KeyValuePairError; - - fn try_from_iter>(iter: I) -> Result { - let pairs = iter - .into_iter() - .map(KeyValuePair::try_from) - .collect::, KeyValuePairError>>()?; - - Ok(Self::from_iter(pairs)) - } + /// Returns whether the list contains a key/value pair with a specific [`Key`]. + /// + /// Returns `false` if `key` cannot be parsed as a valid [`Key`]. + // TODO: Does anyone actually use this API? + fn contains_str_key(&self, key: &str) -> bool; } - -impl From> for BTreeMap -where - T: Value, -{ - fn from(value: KeyValuePairs) -> Self { - value - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) +impl KeyValuePairsExt for KeyValuePairs { + fn to_unvalidated(&self) -> BTreeMap { + self.iter() + .map(|(key, value)| (key.to_string(), value.to_string())) .collect() } -} - -impl Deref for KeyValuePairs -where - T: Value, -{ - type Target = BTreeMap; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl KeyValuePairs -where - T: Value + std::default::Default, -{ - /// Creates a new empty list of [`KeyValuePair`]s. - pub fn new() -> Self { - Self::default() - } - - /// Creates a new list of [`KeyValuePair`]s from `pairs`. - pub fn new_with(pairs: BTreeSet>) -> Self { - Self::from_iter(pairs) - } - /// Extends `self` with `other`. - pub fn extend(&mut self, other: Self) { - self.0.extend(other.0); - } - - /// Inserts a new [`KeyValuePair`] into the list of pairs. - /// - /// This function overwrites any existing key/value pair. To avoid - /// overwriting existing pairs, either use [`KeyValuePairs::contains`] or - /// [`KeyValuePairs::contains_key`] before inserting or try to insert - /// fallible via [`KeyValuePairs::try_insert`]. - pub fn insert(&mut self, kvp: KeyValuePair) -> &mut Self { - self.0.insert(kvp.key, kvp.value); - self - } - - /// Tries to insert a new [`KeyValuePair`] into the list of pairs. - /// - /// If the list already had this key present, nothing is updated, and an - /// error is returned. - pub fn try_insert(&mut self, kvp: KeyValuePair) -> Result<(), KeyValuePairsError> { - ensure!(!self.0.contains_key(&kvp.key), KeyAlreadyExistsSnafu); - self.insert(kvp); - Ok(()) - } - - /// Returns if the list contains a specific [`KeyValuePair`]. - pub fn contains(&self, kvp: impl TryInto>) -> bool { - let Ok(kvp) = kvp.try_into() else { - return false; - }; - let Some(value) = self.get(&kvp.key) else { + fn contains_str_key(&self, key: &str) -> bool { + // We could avoid this clone by providing an UnvalidatedKeyRef and ensure that Key: Borrow + let Ok(key) = key.parse::() else { + // If the key cannot be parsed then it cannot, by definition, possibly exist in the map return false; }; - value == &kvp.value - } - - /// Returns if the list contains a key/value pair with a specific [`Key`]. - pub fn contains_key(&self, key: impl TryInto) -> bool { - let Ok(key) = key.try_into() else { - return false; - }; - - self.0.contains_key(&key) - } - - /// Returns an [`Iterator`] over [`KeyValuePairs`] yielding a reference to every [`KeyValuePair`] contained within. - pub fn iter(&self) -> impl Iterator> + '_ { - self.0.iter().map(|(k, v)| KeyValuePair { - key: k.clone(), - value: v.clone(), - }) + self.contains_key(&key) } } -impl IntoIterator for KeyValuePairs -where - T: Value, -{ - type IntoIter = - std::iter::Map, fn((Key, T)) -> Self::Item>; - type Item = KeyValuePair; - - /// Returns a consuming [`Iterator`] over [`KeyValuePairs`] moving every [`KeyValuePair`] out. - /// The [`KeyValuePairs`] cannot be used again after calling this. - fn into_iter(self) -> Self::IntoIter { - self.0 - .into_iter() - .map(|(key, value)| KeyValuePair { key, value }) +impl<'a, V: Value> TryFromIterator<(&'a str, &'a str)> for KeyValuePairs { + type Error = KeyValuePairError; + + fn try_from_iter>( + iter: I, + ) -> Result { + iter.into_iter() + .map(KeyValuePair::try_from) + .collect::>>() } } @@ -406,26 +241,12 @@ mod test { fn try_from_tuple() { let label = Label::try_from(("stackable.tech/vendor", "Stackable")).unwrap(); - assert_eq!( - label.key(), - &Key::from_str("stackable.tech/vendor").unwrap() - ); - assert_eq!(label.value(), &LabelValue::from_str("Stackable").unwrap()); + assert_eq!(label.key, Key::from_str("stackable.tech/vendor").unwrap()); + assert_eq!(label.value, LabelValue::from_str("Stackable").unwrap()); assert_eq!(label.to_string(), "stackable.tech/vendor=Stackable"); } - #[test] - fn labels_from_array() { - let labels = Labels::try_from([ - ("stackable.tech/managed-by", "stackablectl"), - ("stackable.tech/vendor", "Stackable"), - ]) - .unwrap(); - - assert_eq!(labels.len(), 2); - } - #[test] fn labels_from_iter() { let labels = Labels::from_iter([ @@ -443,28 +264,26 @@ mod test { ("stackable.tech/vendor", "Stackable"), ]); - let labels = Labels::try_from(map).unwrap(); + let labels = Labels::try_from_iter(map).unwrap(); assert_eq!(labels.len(), 2); } #[test] - fn labels_into_map() { - let labels = Labels::try_from([ - ("stackable.tech/managed-by", "stackablectl"), - ("stackable.tech/vendor", "Stackable"), - ]) - .unwrap(); + fn labels_to_unvalidated() { + let labels = Labels::from_iter([ + KeyValuePair::try_from(("stackable.tech/managed-by", "stackablectl")).unwrap(), + KeyValuePair::try_from(("stackable.tech/vendor", "Stackable")).unwrap(), + ]); - let map: BTreeMap = labels.into(); + let map = labels.to_unvalidated(); assert_eq!(map.len(), 2); } #[test] fn contains() { - let labels = Labels::common("test", "test-01").unwrap(); + let labels = label::well_known::sets::common("test", "test-01").unwrap(); - assert!(labels.contains(("app.kubernetes.io/name", "test"))); - assert!(labels.contains_key("app.kubernetes.io/instance")) + assert!(labels.contains_str_key("app.kubernetes.io/instance")) } #[test] @@ -498,10 +317,8 @@ mod test { Labels::try_from_iter([("a", "b"), ("b", "a"), ("c", "c")]).unwrap(); merged_labels.extend(Labels::try_from_iter([("a", "a"), ("b", "b"), ("d", "d")]).unwrap()); assert_eq!( - BTreeMap::from(merged_labels), - BTreeMap::from( - Labels::try_from_iter([("a", "a"), ("b", "b"), ("c", "c"), ("d", "d")]).unwrap() - ) + merged_labels, + Labels::try_from_iter([("a", "a"), ("b", "b"), ("c", "c"), ("d", "d")]).unwrap() ) } }