diff --git a/Cargo.lock b/Cargo.lock index 609c8d2e..e6845f65 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 = "agent-common" @@ -53,12 +53,12 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom 0.2.15", + "getrandom 0.3.3", "once_cell", "version_check", "zerocopy", @@ -96,9 +96,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -111,44 +111,44 @@ 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.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", - "once_cell", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "argh" @@ -170,7 +170,7 @@ dependencies = [ "argh_shared", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -184,9 +184,9 @@ dependencies = [ [[package]] name = "assert_cmd" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" +checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66" dependencies = [ "anstyle", "bstr", @@ -206,18 +206,18 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -228,15 +228,15 @@ 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 = "aws-config" -version = "1.6.1" +version = "1.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c39646d1a6b51240a1a23bb57ea4eebede7e16fbc237fdc876980233dcecb4f" +checksum = "c478f5b10ce55c9a33f87ca3404ca92768b144fc1bfdede7c0121214a8283a25" dependencies = [ "aws-credential-types", "aws-runtime", @@ -259,9 +259,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.2" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4471bef4c22a06d2c7a1b6492493d3fdf24a805323109d6874f9c94d5906ac14" +checksum = "1541072f81945fa1251f8795ef6c92c4282d74d59f88498ae7d4bf00f0ebdad9" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -271,9 +271,9 @@ dependencies = [ [[package]] name = "aws-lc-fips-sys" -version = "0.13.5" +version = "0.13.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d9c2e952a1f57e8cbc78b058a968639e70c4ce8b9c0a5e6363d4e5670eed795" +checksum = "2608e5a7965cc9d58c56234d346c9c89b824c4c8652b6f047b3bd0a777c0644f" dependencies = [ "bindgen", "cc", @@ -285,9 +285,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.12.6" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabb68eb3a7aa08b46fddfd59a3d55c978243557a90ab804769f7e20e67d2b01" +checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" dependencies = [ "aws-lc-fips-sys", "aws-lc-sys", @@ -297,9 +297,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.27.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77926887776171ced7d662120a75998e444d3750c951abfe07f90da130514b1f" +checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" dependencies = [ "bindgen", "cc", @@ -310,9 +310,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.5.6" +version = "1.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aff45ffe35196e593ea3b9dd65b320e51e2dda95aff4390bc459e461d09c6ad" +checksum = "c034a1bc1d70e16e7f4e4caf7e9f7693e4c9c24cd91cf17c2a0b21abaebc7c8b" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -327,7 +327,6 @@ dependencies = [ "fastrand", "http 0.2.12", "http-body 0.4.6", - "once_cell", "percent-encoding", "pin-project-lite", "tracing", @@ -336,9 +335,9 @@ dependencies = [ [[package]] name = "aws-sdk-cloudformation" -version = "1.71.0" +version = "1.89.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52b631dfb3e68c071e919772ad16f31859bd804f3d065c1cf0f188a9c684e323" +checksum = "7aa6e704909e0024e26f886c2ecd3259796a4e3a500d48abe28347c2f8b88acd" dependencies = [ "aws-credential-types", "aws-runtime", @@ -353,16 +352,15 @@ dependencies = [ "aws-types", "fastrand", "http 0.2.12", - "once_cell", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-cloudwatchlogs" -version = "1.76.0" +version = "1.98.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7a8df7f19f2ff90191c905fefb8cf0ff512ad7c2cc92c422240ff5b114750c" +checksum = "3c641af2b0ff6e561a1bf0a52d5ee2cca5b376ea016f5be80c255394027dd882" dependencies = [ "aws-credential-types", "aws-runtime", @@ -377,16 +375,15 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "once_cell", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-ec2" -version = "1.122.0" +version = "1.159.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6c3ef2470307bdeb6471d66d5c28aee1e1b10d7f752d0fd9da6cccc5791f9b3" +checksum = "ed753fc534bbb68760d5b2e3e46f81e5ccb9a96d8359b9ce793e123cb3073e8e" dependencies = [ "aws-credential-types", "aws-runtime", @@ -401,16 +398,15 @@ dependencies = [ "aws-types", "fastrand", "http 0.2.12", - "once_cell", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-ecs" -version = "1.74.0" +version = "1.93.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54196e7685f46c8ece4d78be027de01458f133d32c94c5c148fcb51836a076b4" +checksum = "4ad82e2d5c729d0ee9d39817d69de496b2c628c431d7d1f5b81772241ebd49cb" dependencies = [ "aws-credential-types", "aws-runtime", @@ -424,16 +420,15 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "once_cell", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-eks" -version = "1.83.0" +version = "1.102.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "438965cf3c3ed87fbbe15180c8ba9f426163feec8e24f5ae4d76ccb3f715fc89" +checksum = "d59bca16518ce2a9e1e47f7aea523ac8260609720a06e5833077611549fb3b6d" dependencies = [ "aws-credential-types", "aws-runtime", @@ -447,16 +442,15 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "once_cell", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-iam" -version = "1.67.0" +version = "1.86.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758e1427b5bcd07a20702262696f42594ca1ecd3cbd35c85ab308d3aa6f2a5c1" +checksum = "f685fcb60bd6533f530a93d18328fb28cc8bbce90e5feea75b5dfd4b4103290d" dependencies = [ "aws-credential-types", "aws-runtime", @@ -471,16 +465,15 @@ dependencies = [ "aws-types", "fastrand", "http 0.2.12", - "once_cell", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-secretsmanager" -version = "1.68.0" +version = "1.84.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c18fb03c6aad6a5de2a36e44eebc83640eea9a025bf407579f503b5dc4cd0b0" +checksum = "fe81358a0f6d26531e2541346af4c4d747e16f55d176658f7e141c0383905f88" dependencies = [ "aws-credential-types", "aws-runtime", @@ -494,16 +487,15 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "once_cell", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-ssm" -version = "1.71.0" +version = "1.89.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07c58e00d2059c0080d11c40ffabb7a4802f7ddf35e04d181f0f090eaf20cab" +checksum = "b8dd31cfdf7ee1895fad9bef897281b04f3cb863c032b1dc9ad716ec81b98abf" dependencies = [ "aws-credential-types", "aws-runtime", @@ -517,16 +509,15 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "once_cell", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sts" -version = "1.65.0" +version = "1.82.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a78a8f50a1630db757b60f679c8226a8a70ee2ab5f5e6e51dc67f6c61c7cfd" +checksum = "2194426df72592f91df0cda790cb1e571aa87d66cecfea59a64031b58145abe3" dependencies = [ "aws-credential-types", "aws-runtime", @@ -541,16 +532,15 @@ dependencies = [ "aws-types", "fastrand", "http 0.2.12", - "once_cell", "regex-lite", "tracing", ] [[package]] name = "aws-sigv4" -version = "1.3.0" +version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d03c3c05ff80d54ff860fe38c726f6f494c639ae975203a101335f223386db" +checksum = "084c34162187d39e3740cb635acd73c4e3a551a36146ad6fe8883c929c9f876c" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", @@ -563,7 +553,6 @@ dependencies = [ "hmac", "http 0.2.12", "http 1.3.1", - "once_cell", "percent-encoding", "sha2", "time", @@ -583,9 +572,9 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.8" +version = "0.60.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c45d3dddac16c5c59d553ece225a88870cf81b7b813c9cc17b78cf4685eac7a" +checksum = "182b03393e8c677347fb5705a04a9392695d47d20ef0a2f8cfe28c8e6b9b9778" dependencies = [ "aws-smithy-types", "bytes", @@ -594,9 +583,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.62.0" +version = "0.62.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5949124d11e538ca21142d1fba61ab0a2a2c1bc3ed323cdb3e4b878bfb83166" +checksum = "7c4dacf2d38996cf729f55e7a762b30918229917eca115de45dfa8dfb97796c9" dependencies = [ "aws-smithy-eventstream", "aws-smithy-runtime-api", @@ -607,7 +596,6 @@ dependencies = [ "http 0.2.12", "http 1.3.1", "http-body 0.4.6", - "once_cell", "percent-encoding", "pin-project-lite", "pin-utils", @@ -616,44 +604,44 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8aff1159006441d02e57204bf57a1b890ba68bedb6904ffd2873c1c4c11c546b" +checksum = "4fdbad9bd9dbcc6c5e68c311a841b54b70def3ca3b674c42fbebb265980539f8" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", "aws-smithy-types", "h2", "http 1.3.1", - "hyper 1.6.0", - "hyper-rustls 0.27.5", + "hyper 1.7.0", + "hyper-rustls 0.27.7", "hyper-util", "pin-project-lite", - "rustls 0.23.25", + "rustls 0.23.31", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", + "tokio-rustls 0.26.2", "tower 0.5.2", "tracing", ] [[package]] name = "aws-smithy-json" -version = "0.61.3" +version = "0.61.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92144e45819cae7dc62af23eac5a038a58aa544432d2102609654376a900bd07" +checksum = "a16e040799d29c17412943bdbf488fd75db04112d0c0d4b9290bacf5ae0014b9" dependencies = [ "aws-smithy-types", ] [[package]] name = "aws-smithy-observability" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445d065e76bc1ef54963db400319f1dd3ebb3e0a74af20f7f7630625b0cc7cc0" +checksum = "9364d5989ac4dd918e5cc4c4bdcc61c9be17dcd2586ea7f69e348fc7c6cab393" dependencies = [ "aws-smithy-runtime-api", - "once_cell", ] [[package]] @@ -668,9 +656,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0152749e17ce4d1b47c7747bdfec09dac1ccafdcbc741ebf9daa2a373356730f" +checksum = "a3d57c8b53a72d15c8e190475743acf34e4996685e346a3448dd54ef696fc6e0" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -684,7 +672,6 @@ dependencies = [ "http 1.3.1", "http-body 0.4.6", "http-body 1.0.1", - "once_cell", "pin-project-lite", "pin-utils", "tokio", @@ -693,9 +680,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.7.4" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da37cf5d57011cb1753456518ec76e31691f1f474b73934a284eb2a1c76510f" +checksum = "07f5e0fc8a6b3f2303f331b94504bbf754d85488f402d6f1dd7a6080f99afe56" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -710,9 +697,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.3.0" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836155caafba616c0ff9b07944324785de2ab016141c3550bd1c07882f8cee8f" +checksum = "d498595448e43de7f4296b7b7a18a8a02c61ec9349128c80a368f7c3b4ab11a8" dependencies = [ "base64-simd", "bytes", @@ -736,18 +723,18 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.9" +version = "0.60.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" +checksum = "3db87b96cb1b16c024980f133968d52882ca0daaee3a086c6decc500f6c99728" dependencies = [ "xmlparser", ] [[package]] name = "aws-types" -version = "1.3.6" +version = "1.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3873f8deed8927ce8d04487630dc9ff73193bab64742a61d050e57a68dec4125" +checksum = "b069d19bf01e46298eaedd7c6f283fe565a59263e53eebec945f3e6398f42390" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -763,16 +750,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "instant", - "rand", + "rand 0.8.5", ] [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -824,15 +811,15 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.100", + "syn 2.0.106", "which", ] [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" [[package]] name = "block-buffer" @@ -882,7 +869,7 @@ dependencies = [ "test-agent", "testsys-model", "tokio", - "toml", + "toml 0.5.11", "tough", "url", "uuid", @@ -903,11 +890,19 @@ dependencies = [ "testsys-model", ] +[[package]] +name = "bottlerocket-variant" +version = "0.1.0" +dependencies = [ + "serde", + "snafu", +] + [[package]] name = "bstr" -version = "1.11.3" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", "regex-automata", @@ -923,21 +918,21 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.100", + "syn 2.0.106", "testsys-model", ] [[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 = "bytecount" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "byteorder" @@ -963,9 +958,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.17" +version = "1.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" dependencies = [ "jobserver", "libc", @@ -983,9 +978,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" @@ -995,14 +990,16 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link", ] @@ -1019,9 +1016,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.34" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff" +checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" dependencies = [ "clap_builder", "clap_derive", @@ -1029,9 +1026,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.34" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" dependencies = [ "anstream", "anstyle", @@ -1041,21 +1038,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[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 = "cli" @@ -1069,7 +1066,7 @@ dependencies = [ "log", "selftest", "serde_json", - "terminal_size", + "terminal_size 0.3.0", "testsys-model", "tokio", ] @@ -1085,16 +1082,16 @@ dependencies = [ [[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 = "configuration-derive" version = "0.0.17" dependencies = [ "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -1140,9 +1137,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -1165,9 +1162,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", ] @@ -1203,7 +1200,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -1214,20 +1211,20 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "data-encoding" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "deranged" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] @@ -1268,7 +1265,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -1285,9 +1282,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[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 = "either" @@ -1307,9 +1304,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.7" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", @@ -1335,12 +1332,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1351,21 +1348,21 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "filetime" -version = "0.2.25" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "flate2" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "miniz_oxide", @@ -1448,7 +1445,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -1493,13 +1490,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1509,9 +1508,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -1522,9 +1523,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" @@ -1541,9 +1542,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -1558,6 +1559,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "handlebars" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b" +dependencies = [ + "log", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1568,6 +1583,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + [[package]] name = "heck" version = "0.4.1" @@ -1694,7 +1715,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -1703,19 +1724,21 @@ dependencies = [ [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http 1.3.1", "http-body 1.0.1", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -1739,15 +1762,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "futures-util", "http 1.3.1", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", - "rustls 0.23.25", + "rustls 0.23.31", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", @@ -1770,18 +1792,23 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", + "futures-core", "futures-util", "http 1.3.1", "http-body 1.0.1", - "hyper 1.6.0", + "hyper 1.7.0", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -1789,9 +1816,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.62" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1813,21 +1840,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -1836,31 +1864,11 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -1868,67 +1876,54 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "potential_utf", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -1948,9 +1943,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1958,12 +1953,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.5.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.5", ] [[package]] @@ -1975,12 +1970,33 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -2004,9 +2020,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.5" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c102670231191d07d37a35af3eb77f1f0dbf7a71be51a962dcd57ea607be7260" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ "jiff-static", "log", @@ -2017,21 +2033,22 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.5" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cdde31a9d349f1b1f51a0b3714a5940ac022976f4b49485fc04be052b183b4c" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ + "getrandom 0.3.3", "libc", ] @@ -2053,7 +2070,7 @@ checksum = "ec9ad60d674508f3ca8f380a928cfe7b096bc729c4e2dbfe3852bc45da3ab30b" dependencies = [ "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2066,7 +2083,7 @@ dependencies = [ "pest_derive", "regex", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2116,19 +2133,19 @@ dependencies = [ "kube-core", "pem", "pin-project", - "rand", + "rand 0.8.5", "rustls 0.21.12", - "rustls-pemfile 1.0.4", + "rustls-pemfile", "secrecy", "serde", "serde_json", "serde_yaml", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-tungstenite", "tokio-util", "tower 0.4.13", - "tower-http", + "tower-http 0.4.4", "tracing", ] @@ -2147,7 +2164,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2160,7 +2177,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -2174,7 +2191,7 @@ dependencies = [ "backoff", "derivative", "futures", - "hashbrown", + "hashbrown 0.14.5", "json-patch", "k8s-openapi", "kube-client", @@ -2183,7 +2200,7 @@ dependencies = [ "serde", "serde_json", "smallvec", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-util", "tracing", @@ -2203,25 +2220,25 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.171" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libloading" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ "bitflags", "libc", @@ -2234,17 +2251,23 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "litemap" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +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", @@ -2256,6 +2279,12 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "maplit" version = "1.0.2" @@ -2264,9 +2293,9 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[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" @@ -2282,22 +2311,22 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] @@ -2366,6 +2395,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "openssl-probe" version = "0.1.6" @@ -2400,9 +2435,9 @@ dependencies = [ [[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", @@ -2410,9 +2445,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", @@ -2421,6 +2456,14 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parse-datetime" +version = "0.1.0" +dependencies = [ + "chrono", + "snafu", +] + [[package]] name = "pem" version = "3.0.5" @@ -2439,20 +2482,20 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.14" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" +checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", - "thiserror", + "thiserror 2.0.16", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.7.14" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d214365f632b123a47fd913301e14c946c61d1c183ee245fa76eb752e59a02dd" +checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" dependencies = [ "pest", "pest_generator", @@ -2460,24 +2503,23 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.14" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb55586734301717aea2ac313f50b2eb8f60d2fc3dc01d190eefa2e625f60c4e" +checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "pest_meta" -version = "2.7.14" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75da2a70cf4d9cb76833c990ac9cd3923c9a8905a8929789ce347c84564d03d" +checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" dependencies = [ - "once_cell", "pest", "sha2", ] @@ -2499,7 +2541,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -2516,9 +2558,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" @@ -2529,6 +2571,15 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2537,9 +2588,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] @@ -2573,12 +2624,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.31" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -2607,58 +2658,80 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] +[[package]] +name = "pubsys-config" +version = "0.1.0" +dependencies = [ + "chrono", + "home", + "lazy_static", + "log", + "parse-datetime", + "serde", + "serde_yaml", + "snafu", + "toml 0.8.23", + "url", +] + [[package]] name = "quinn" -version = "0.11.5" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" dependencies = [ "bytes", + "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls 0.23.25", - "socket2", - "thiserror", + "rustls 0.23.31", + "socket2 0.5.10", + "thiserror 2.0.16", "tokio", "tracing", + "web-time", ] [[package]] name = "quinn-proto" -version = "0.11.8" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" dependencies = [ "bytes", - "rand", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", "ring", "rustc-hash 2.1.1", - "rustls 0.23.25", + "rustls 0.23.31", + "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.16", "tinyvec", "tracing", + "web-time", ] [[package]] name = "quinn-udp" -version = "0.5.11" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5" +checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.5.10", "tracing", "windows-sys 0.59.0", ] @@ -2674,9 +2747,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" @@ -2685,8 +2758,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -2696,7 +2779,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -2705,14 +2798,23 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", ] [[package]] name = "redox_syscall" -version = "0.5.10" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags", ] @@ -2754,9 +2856,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.9" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "base64 0.22.1", "bytes", @@ -2766,19 +2868,15 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.6.0", - "hyper-rustls 0.27.5", + "hyper 1.7.0", + "hyper-rustls 0.27.7", "hyper-util", - "ipnet", "js-sys", "log", - "mime", - "once_cell", "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.25", - "rustls-pemfile 2.2.0", + "rustls 0.23.31", "rustls-pki-types", "serde", "serde_json", @@ -2787,6 +2885,8 @@ dependencies = [ "tokio", "tokio-rustls 0.26.2", "tokio-util", + "tower 0.5.2", + "tower-http 0.6.6", "tower-service", "url", "wasm-bindgen", @@ -2794,7 +2894,6 @@ dependencies = [ "wasm-streams", "web-sys", "webpki-roots", - "windows-registry", ] [[package]] @@ -2821,7 +2920,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted 0.9.0", "windows-sys 0.52.0", @@ -2835,9 +2934,9 @@ checksum = "a157657054ffe556d8858504af8a672a054a6e0bd9e8ee531059100c0fa11bb2" [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -2869,10 +2968,23 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.15", "windows-sys 0.59.0", ] +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.60.2", +] + [[package]] name = "rustls" version = "0.21.12" @@ -2887,16 +2999,16 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.25" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.1", + "rustls-webpki 0.103.4", "subtle", "zeroize", ] @@ -2908,7 +3020,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ "openssl-probe", - "rustls-pemfile 1.0.4", + "rustls-pemfile", "schannel", "security-framework 2.11.1", ] @@ -2922,7 +3034,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.2.0", + "security-framework 3.3.0", ] [[package]] @@ -2935,20 +3047,15 @@ dependencies = [ ] [[package]] -name = "rustls-pemfile" -version = "2.2.0" +name = "rustls-pki-types" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ - "rustls-pki-types", + "web-time", + "zeroize", ] -[[package]] -name = "rustls-pki-types" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" - [[package]] name = "rustls-webpki" version = "0.101.7" @@ -2961,9 +3068,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.1" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "aws-lc-rs", "ring", @@ -2973,9 +3080,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -3066,12 +3173,12 @@ 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.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -3135,7 +3242,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -3151,9 +3258,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", @@ -3170,6 +3277,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3208,9 +3324,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -3225,34 +3341,32 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "snafu" -version = "0.8.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" +checksum = "0062a372b26c4a6e9155d099a3416d732514fd47ae2f235b3695b820afcee74a" dependencies = [ + "backtrace", "futures-core", "pin-project", "snafu-derive", @@ -3260,26 +3374,36 @@ dependencies = [ [[package]] name = "snafu-derive" -version = "0.8.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" +checksum = "7e5fd9e3263fc19d73abd5107dbd4d43e37949212d2b15d4d334ee5db53022b8" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "socket2" -version = "0.5.8" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -3311,7 +3435,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -3333,9 +3457,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.100" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -3353,13 +3477,13 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -3399,16 +3523,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.15.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ - "cfg-if", "fastrand", - "getrandom 0.2.15", + "getrandom 0.3.3", "once_cell", - "rustix", - "windows-sys 0.59.0", + "rustix 1.0.8", + "windows-sys 0.60.2", ] [[package]] @@ -3417,10 +3540,20 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ - "rustix", + "rustix 0.38.44", "windows-sys 0.48.0", ] +[[package]] +name = "terminal_size" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +dependencies = [ + "rustix 1.0.8", + "windows-sys 0.60.2", +] + [[package]] name = "termtree" version = "0.5.1" @@ -3464,6 +3597,54 @@ dependencies = [ "tokio", ] +[[package]] +name = "testsys" +version = "0.1.0" +dependencies = [ + "async-trait", + "aws-config", + "aws-sdk-ec2", + "base64 0.22.1", + "bottlerocket-types", + "bottlerocket-variant", + "clap", + "env_logger", + "fastrand", + "futures", + "handlebars", + "log", + "maplit", + "pubsys-config", + "serde", + "serde_json", + "serde_plain", + "serde_yaml", + "snafu", + "terminal_size 0.4.3", + "testsys-config", + "testsys-model", + "tokio", + "unescape", + "url", +] + +[[package]] +name = "testsys-config" +version = "0.1.0" +dependencies = [ + "bottlerocket-types", + "bottlerocket-variant", + "handlebars", + "log", + "maplit", + "serde", + "serde_plain", + "serde_yaml", + "snafu", + "testsys-model", + "toml 0.8.23", +] + [[package]] name = "testsys-model" version = "0.0.17" @@ -3501,7 +3682,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl 2.0.16", ] [[package]] @@ -3512,7 +3702,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -3547,9 +3748,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -3557,9 +3758,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -3572,26 +3773,28 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.2" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "pin-project-lite", "signal-hook-registry", - "socket2", + "slab", + "socket2 0.6.0", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "tokio-io-timeout" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +checksum = "0bd86198d9ee903fedd2f9a2e72014287c0d9167e4ae43b5853007205dda1b76" dependencies = [ "pin-project-lite", "tokio", @@ -3605,7 +3808,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -3624,7 +3827,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.25", + "rustls 0.23.31", "tokio", ] @@ -3642,9 +3845,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -3663,6 +3866,47 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "topological-sort" version = "0.2.2" @@ -3690,7 +3934,7 @@ dependencies = [ "pem", "percent-encoding", "reqwest", - "rustls 0.23.25", + "rustls 0.23.31", "serde", "serde_json", "serde_plain", @@ -3727,6 +3971,11 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", "tower-layer", "tower-service", ] @@ -3752,6 +4001,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -3778,20 +4045,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.100", + "syn 2.0.106", ] [[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", ] @@ -3814,9 +4081,9 @@ dependencies = [ "http 0.2.12", "httparse", "log", - "rand", + "rand 0.8.5", "sha1", - "thiserror", + "thiserror 1.0.69", "url", "utf-8", ] @@ -3839,6 +4106,12 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "unescape" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccb97dac3243214f8d8507998906ca3e2e0b900bf9bf4870477f125b82e68f6e" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -3887,6 +4160,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -3901,12 +4175,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -3921,9 +4189,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" dependencies = [ "getrandom 0.3.3", "js-sys", @@ -3973,9 +4241,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" @@ -4008,7 +4276,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", "wasm-bindgen-shared", ] @@ -4043,7 +4311,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4080,11 +4348,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" -version = "0.26.8" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" dependencies = [ "rustls-pki-types", ] @@ -4098,61 +4376,75 @@ dependencies = [ "either", "home", "once_cell", - "rustix", + "rustix 0.38.44", ] [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] name = "windows-core" -version = "0.52.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-targets 0.52.6", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] -name = "windows-link" -version = "0.1.1" +name = "windows-implement" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] [[package]] -name = "windows-registry" -version = "0.2.0" +name = "windows-interface" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ - "windows-result", - "windows-strings", - "windows-targets 0.52.6", + "proc-macro2", + "quote", + "syn 2.0.106", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-result" -version = "0.2.0" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-targets 0.52.6", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-result", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -4182,6 +4474,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -4206,13 +4507,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4225,6 +4543,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4237,6 +4561,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4249,12 +4579,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4267,6 +4609,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4279,6 +4627,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4291,6 +4645,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4303,6 +4663,21 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -4312,27 +4687,20 @@ dependencies = [ "bitflags", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "xattr" -version = "1.4.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909" +checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" dependencies = [ "libc", - "linux-raw-sys", - "rustix", + "rustix 1.0.8", ] [[package]] @@ -4343,9 +4711,9 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", @@ -4355,35 +4723,34 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", "synstructure", ] [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -4403,7 +4770,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", "synstructure", ] @@ -4413,11 +4780,22 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", @@ -4426,11 +4804,11 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] diff --git a/Cargo.toml b/Cargo.toml index 7228c1dc..296f55ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,11 @@ members = [ "controller", "model", "selftest", + "testsys-launcher/parse-datetime", + "testsys-launcher/pubsys-config", + "testsys-launcher/bottlerocket-variant", + "testsys-launcher/testsys-config", + "testsys-launcher/testsys", ] resolver = "2" diff --git a/Makefile b/Makefile index 69ab03b2..53535ca2 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ SHELL = /bin/bash TOP := $(dir $(firstword $(MAKEFILE_LIST))) # Variables we update as newer versions are released -BOTTLEROCKET_SDK_VERSION = v0.50.1 +BOTTLEROCKET_SDK_VERSION = v0.62.0 BOTTLEROCKET_SDK_ARCH = $(TESTSYS_BUILD_HOST_UNAME_ARCH) BOTTLEROCKET_TOOLS_VERSION ?= v0.10.0 diff --git a/agent/builder-derive/src/derive.rs b/agent/builder-derive/src/derive.rs index 6e5f126b..8faf3fde 100644 --- a/agent/builder-derive/src/derive.rs +++ b/agent/builder-derive/src/derive.rs @@ -33,7 +33,7 @@ pub(crate) fn build_struct(ast: &syn::DeriveInput) -> TokenStream { None } }) - .last() + .next_back() .expect("`crd` is a required attribute (Test, Resource)") .value(); diff --git a/agent/utils/src/aws.rs b/agent/utils/src/aws.rs index 687a2fd8..8bca3745 100644 --- a/agent/utils/src/aws.rs +++ b/agent/utils/src/aws.rs @@ -34,7 +34,7 @@ pub async fn aws_config( region ); - let mut config_loader = aws_config::defaults(BehaviorVersion::v2025_01_17()).retry_config( + let mut config_loader = aws_config::defaults(BehaviorVersion::v2025_08_07()).retry_config( RetryConfig::standard() .with_retry_mode(RetryMode::Adaptive) .with_max_attempts(15), @@ -95,7 +95,10 @@ pub async fn aws_config( .set_duration_seconds(*assume_role_session_duration) .send() .await - .context(error::AssumeRoleSnafu { role_arn })? + .map_err(|source| Error::AssumeRole { + role_arn: role_arn.to_string(), + source: Box::new(source), + })? .credentials() .context(error::CredentialsMissingSnafu { role_arn })? .clone(); diff --git a/agent/utils/src/error.rs b/agent/utils/src/error.rs index a13bef6a..105e4c6a 100644 --- a/agent/utils/src/error.rs +++ b/agent/utils/src/error.rs @@ -15,7 +15,7 @@ use std::string::FromUtf8Error; pub enum Error { AssumeRole { role_arn: String, - source: StsSdkError, + source: Box>, }, #[snafu(display( @@ -27,7 +27,7 @@ pub enum Error { AttachRolePolicy { role_name: String, policy_arn: String, - source: IamSdkError, + source: Box>, }, #[snafu(display("Failed to decode base64 blob: {}", source))] @@ -48,7 +48,7 @@ pub enum Error { #[snafu(display("Failed to send create SSM command: {}", source))] CreateSsmActivation { - source: aws_sdk_ssm::error::SdkError, + source: Box>, }, #[snafu(display( @@ -60,7 +60,7 @@ pub enum Error { CreateRole { role_name: String, role_policy: String, - source: IamSdkError, + source: Box>, }, #[snafu(display("Credentials were missing for assumed role '{}'", role_arn))] @@ -71,13 +71,13 @@ pub enum Error { #[snafu(display("Unable to get managed instance information: {}", source))] GetManagedInstanceInfo { - source: SsmSdkError, + source: Box>, }, #[snafu(display("Unable to get SSM role '{}': {}", role_name, source))] GetSSMRole { role_name: String, - source: IamSdkError, + source: Box>, }, #[snafu(display("{} was missing from {}", what, from))] diff --git a/agent/utils/src/ssm.rs b/agent/utils/src/ssm.rs index 67db1b4f..25503fb3 100644 --- a/agent/utils/src/ssm.rs +++ b/agent/utils/src/ssm.rs @@ -1,4 +1,4 @@ -use crate::error::{self, Result}; +use crate::error::{self, Error, Result}; use aws_sdk_iam::error::SdkError as IamSdkError; use aws_sdk_ssm::types::{InstanceInformation, InstanceInformationStringFilter, Tag}; use log::info; @@ -40,15 +40,16 @@ pub async fn ensure_ssm_service_role(iam_client: &aws_sdk_iam::Client) -> Result .assume_role_policy_document(&assume_role_doc) .send() .await - .context(error::CreateRoleSnafu { - role_name: SSM_MANAGED_INSTANCE_SERVICE_ROLE_NAME, + .map_err(|source| Error::CreateRole { + source: Box::new(source), + role_name: SSM_MANAGED_INSTANCE_SERVICE_ROLE_NAME.to_string(), role_policy: assume_role_doc, })?; } e => { - return Err(error::Error::GetSSMRole { + return Err(Error::GetSSMRole { role_name: SSM_MANAGED_INSTANCE_SERVICE_ROLE_NAME.to_string(), - source: e, + source: Box::new(e), }); } } @@ -61,9 +62,10 @@ pub async fn ensure_ssm_service_role(iam_client: &aws_sdk_iam::Client) -> Result .policy_arn(SSM_MANAGED_INSTANCE_POLICY_ARN) .send() .await - .context(error::AttachRolePolicySnafu { - role_name: SSM_MANAGED_INSTANCE_SERVICE_ROLE_NAME, - policy_arn: SSM_MANAGED_INSTANCE_POLICY_ARN, + .map_err(|source| Error::AttachRolePolicy { + source: Box::new(source), + role_name: SSM_MANAGED_INSTANCE_SERVICE_ROLE_NAME.to_string(), + policy_arn: SSM_MANAGED_INSTANCE_POLICY_ARN.to_string(), })?; Ok(()) @@ -87,7 +89,9 @@ pub async fn create_ssm_activation( ) .send() .await - .context(error::CreateSsmActivationSnafu {})?; + .map_err(|source| Error::CreateSsmActivation { + source: Box::new(source), + })?; let activation_id = activations.activation_id.context(error::MissingSnafu { what: "activation id", from: "activations", @@ -118,7 +122,9 @@ pub async fn wait_for_ssm_ready( ) .send() .await - .context(error::GetManagedInstanceInfoSnafu {})?; + .map_err(|source| Error::GetManagedInstanceInfo { + source: Box::new(source), + })?; if let Some(info) = instance_info .instance_information_list() .iter() diff --git a/bottlerocket/agents/src/bin/ecs-test-agent/main.rs b/bottlerocket/agents/src/bin/ecs-test-agent/main.rs index 557cf2ca..59e7a90d 100644 --- a/bottlerocket/agents/src/bin/ecs-test-agent/main.rs +++ b/bottlerocket/agents/src/bin/ecs-test-agent/main.rs @@ -81,7 +81,9 @@ where .launch_type(LaunchType::Ec2) .send() .await - .context(error::TaskRunCreationSnafu)?; + .map_err(|source| Error::TaskRunCreation { + source: Box::new(source), + })?; let task_arns: Vec = run_task_output .tasks() .iter() @@ -146,7 +148,9 @@ async fn test_results( .set_tasks(Some(task_arns.to_vec())) .send() .await - .context(error::TaskDescribeSnafu)? + .map_err(|source| Error::TaskDescribe { + source: Box::new(source), + })? .tasks() .to_owned(); let running_count = tasks @@ -184,7 +188,9 @@ async fn wait_for_registered_containers( .clusters(cluster) .send() .await - .context(error::ClusterDescribeSnafu)? + .map_err(|source| Error::ClusterDescribe { + source: Box::new(source), + })? .clusters() .first() .context(error::NoTaskSnafu)? @@ -234,7 +240,9 @@ async fn create_task_definition(ecs_client: &aws_sdk_ecs::Client) -> Result Result = run_task_output .tasks() .iter() @@ -243,7 +245,9 @@ async fn test_results( .set_tasks(Some(task_arns.to_vec())) .send() .await - .context(error::TaskDescribeSnafu)? + .map_err(|source| Error::TaskDescribe { + source: Box::new(source), + })? .tasks() .to_owned(); let passed_count = tasks @@ -300,7 +304,9 @@ async fn wait_for_cluster_ready( .clusters(cluster) .send() .await - .context(error::ClusterDescribeSnafu)? + .map_err(|source| Error::ClusterDescribe { + source: Box::new(source), + })? .clusters() .first() .context(error::NoTaskSnafu)? @@ -363,7 +369,9 @@ async fn find_task_rev( .family_prefix(task_def_name) .send() .await - .context(error::TaskDefinitionListSnafu)?; + .map_err(|source| Error::TaskDefinitionList { + source: Box::new(source), + })?; let task_revisions = task_revisions.task_definition_arns(); for task_rev_arn in task_revisions { @@ -454,7 +462,9 @@ async fn create_task_definition( .container_definitions(create_container_definition(test_def)?) .send() .await - .context(error::TaskDefinitionCreationSnafu)?; + .map_err(|source| Error::TaskDefinitionCreation { + source: Box::new(source), + })?; if let Some(task_arn) = task_info .task_definition() .context(error::TaskDefinitionMissingSnafu)? diff --git a/bottlerocket/agents/src/bin/migration-test-agent/ssm.rs b/bottlerocket/agents/src/bin/migration-test-agent/ssm.rs index 7936ddd7..0f104093 100644 --- a/bottlerocket/agents/src/bin/migration-test-agent/ssm.rs +++ b/bottlerocket/agents/src/bin/migration-test-agent/ssm.rs @@ -4,7 +4,7 @@ use aws_sdk_ssm::types::{ CommandInvocation, CommandInvocationStatus, DocumentFormat, DocumentType, InstanceInformationStringFilter, }; -use bottlerocket_agents::error; +use bottlerocket_agents::error::{self, Error}; use log::{debug, info}; use maplit::hashmap; use sha2::{Digest, Sha256}; @@ -34,7 +34,9 @@ pub(crate) async fn wait_for_ssm_ready( ) .send() .await - .context(error::SsmDescribeInstanceInfoSnafu)?; + .map_err(|source| Error::SsmDescribeInstanceInfo { + source: Box::new(source), + })?; num_ready = instance_info.instance_information_list().len(); sleep(sec_between_checks); } @@ -73,7 +75,9 @@ pub(crate) async fn create_or_update_ssm_document( .document_format(DocumentFormat::Yaml) .send() .await - .context(error::SsmCreateDocumentSnafu)?; + .map_err(|source| Error::SsmCreateDocument { + source: Box::new(source), + })?; Ok(()) } _ => error::SsmDescribeDocumentSnafu { @@ -120,7 +124,9 @@ pub(crate) async fn create_or_update_ssm_document( .document_format(DocumentFormat::Yaml) .send() .await - .context(error::SsmUpdateDocumentSnafu)?; + .map_err(|source| Error::SsmUpdateDocument { + source: Box::new(source), + })?; Ok(()) } @@ -135,7 +141,9 @@ async fn wait_command_finish( .command_id(cmd_id.to_owned()) .send() .await - .context(error::SsmListCommandInvocationsSnafu)?; + .map_err(|source| Error::SsmListCommandInvocations { + source: Box::new(source), + })?; if let Some(invocations) = cmd_status.command_invocations { if invocations.is_empty() || invocations.iter().any(|i| { @@ -171,7 +179,9 @@ pub(crate) async fn ssm_run_command( .timeout_seconds(30) .send() .await - .context(error::SsmSendCommandSnafu)? + .map_err(|source| Error::SsmSendCommand { + source: Box::new(source), + })? .command() .and_then(|c| c.command_id().map(|s| s.to_string())) .context(error::SsmCommandIdSnafu)?; diff --git a/bottlerocket/agents/src/error.rs b/bottlerocket/agents/src/error.rs index f09c0f49..a707c830 100644 --- a/bottlerocket/agents/src/error.rs +++ b/bottlerocket/agents/src/error.rs @@ -53,7 +53,7 @@ pub enum Error { #[snafu(display("SSM Create Document failed: {}", source))] SsmCreateDocument { - source: SsmSdkError, + source: Box>, }, #[snafu(display("SSM Describe Document failed: {}", message))] @@ -61,19 +61,19 @@ pub enum Error { #[snafu(display("SSM Update Document failed: {}", source))] SsmUpdateDocument { - source: SsmSdkError, + source: Box>, }, #[snafu(display("SSM Send Command failed: {}", source))] SsmSendCommand { - source: SsmSdkError, + source: Box>, }, #[snafu(display("SSM List Command Invocations failed: {}", source))] SsmListCommandInvocations { - source: SsmSdkError< + source: Box, + >>, }, #[snafu(display("No command ID in SSM send command response"))] @@ -93,9 +93,9 @@ pub enum Error { #[snafu(display("SSM Describe Instance Information failed: {}", source))] SsmDescribeInstanceInfo { - source: SsmSdkError< + source: Box, + >>, }, #[snafu(display("Missing instance information from describe-instance-info output"))] @@ -128,54 +128,53 @@ pub enum Error { #[snafu(display("Unable to create task definition: {}", source))] TaskDefinitionCreation { - source: EcsSdkError< + source: Box, + >>, }, #[snafu(display("Unable to describe task definition: {}", source))] TaskDefinitionDescribe { - source: EcsSdkError< + source: Box, + >>, }, #[snafu(display("Unable to list task definitions: {}", source))] TaskDefinitionList { - source: - EcsSdkError, + source: Box>, }, #[snafu(display("Unable to run task: {}", source))] TaskRunCreation { - source: EcsSdkError, + source: Box>, }, #[snafu(display("Unable to update the service: {}", source))] TaskServiceUpdate { - source: EcsSdkError, + source: Box>, }, #[snafu(display("Unable to delete service: {}", source))] TaskServiceDelete { - source: EcsSdkError, + source: Box>, }, #[snafu(display("Unable to get task description: {}", source))] TaskDescribe { - source: EcsSdkError, + source: Box>, }, #[snafu(display("Unable to get cluster description: {}", source))] ClusterDescribe { - source: EcsSdkError, + source: Box>, }, #[snafu(display("Unable to deregister task description: {}", source))] DeregisterTask { - source: EcsSdkError< + source: Box, + >>, }, #[snafu(display("No task running tasks in cluster"))] diff --git a/controller/src/job/error.rs b/controller/src/job/error.rs index 703b9330..bdf2edaa 100644 --- a/controller/src/job/error.rs +++ b/controller/src/job/error.rs @@ -21,8 +21,10 @@ pub(crate) enum JobError { #[snafu(display("Unable to create log event '{}': {:?}", log_event, source))] CreateLogEvent { log_event: String, - source: aws_sdk_cloudwatchlogs::error::SdkError< - aws_sdk_cloudwatchlogs::operation::put_log_events::PutLogEventsError, + source: Box< + aws_sdk_cloudwatchlogs::error::SdkError< + aws_sdk_cloudwatchlogs::operation::put_log_events::PutLogEventsError, + >, >, }, @@ -32,8 +34,10 @@ pub(crate) enum JobError { #[snafu(display("Unable to create log stream '{}': {:?}", log_stream, source))] CreateLogStream { log_stream: String, - source: aws_sdk_cloudwatchlogs::error::SdkError< - aws_sdk_cloudwatchlogs::operation::create_log_stream::CreateLogStreamError, + source: Box< + aws_sdk_cloudwatchlogs::error::SdkError< + aws_sdk_cloudwatchlogs::operation::create_log_stream::CreateLogStreamError, + >, >, }, diff --git a/controller/src/job/mod.rs b/controller/src/job/mod.rs index c2e404ac..d0128c2d 100644 --- a/controller/src/job/mod.rs +++ b/controller/src/job/mod.rs @@ -177,7 +177,7 @@ pub(crate) async fn archive_logs(k8s_client: kube::Client, job_name: &str) -> Jo if !archive_logs { return Ok(()); } - let config = aws_config::defaults(BehaviorVersion::v2025_01_17()) + let config = aws_config::defaults(BehaviorVersion::v2025_08_07()) .load() .await; let client = aws_sdk_cloudwatchlogs::Client::new(&config); @@ -215,7 +215,8 @@ pub(crate) async fn archive_logs(k8s_client: kube::Client, job_name: &str) -> Jo .log_stream_name(&name) .send() .await - .context(error::CreateLogStreamSnafu { + .map_err(|source| JobError::CreateLogStream { + source: Box::new(source), log_stream: name.to_string(), })?; @@ -238,7 +239,10 @@ pub(crate) async fn archive_logs(k8s_client: kube::Client, job_name: &str) -> Jo ) .send() .await - .context(error::CreateLogEventSnafu { log_event: &name })?; + .map_err(|source| JobError::CreateLogEvent { + log_event: name.clone(), + source: Box::new(source), + })?; info!("Archive of '{job_name}' can be found at '{name}'"); diff --git a/deny.toml b/deny.toml index 9d53d5e0..e7e8977d 100644 --- a/deny.toml +++ b/deny.toml @@ -10,6 +10,7 @@ allow = [ #"BSD-2-Clause", "BSD-3-Clause", #"BSL-1.0", + "CDLA-Permissive-2.0", #"CC0-1.0", "ISC", "MIT", @@ -20,7 +21,7 @@ allow = [ #"Zlib" ] -exceptions = [ { allow = [ "MPL-2.0" ], name = "webpki-roots" } ] +# exceptions = [ { allow = [ "MPL-2.0" ], name = "webpki-roots" } ] [bans] multiple-versions = "deny" @@ -33,6 +34,12 @@ skip = [ # tabled uses an older version of heck "heck@0.4.1", + + # duplicate dependencies from different dependency trees + "linux-raw-sys@0.4.15", + "rustix@0.38.44", + "terminal_size@0.3.0", + "toml@0.5.11", ] skip-tree = [ diff --git a/model/src/clients/resource_client.rs b/model/src/clients/resource_client.rs index a5ed17f4..b2c12033 100644 --- a/model/src/clients/resource_client.rs +++ b/model/src/clients/resource_client.rs @@ -255,7 +255,14 @@ impl ResourceClient { } async fn resolve_input_string(&self, input: String) -> Result { - if let Some((resource_name, field_name)) = resource_name_and_field_name(&input)? { + if let Some((resource_name, field_name)) = + resource_name_and_field_name(&input).map_err(|e| { + error::ConfigResolutionSnafu { + what: e.to_string(), + } + .build() + })? + { let resource = self.get(resource_name).await?; let results = resource .created_resource() @@ -274,22 +281,20 @@ impl ResourceClient { } } -fn resource_name_and_field_name(input: &str) -> Result> { +fn resource_name_and_field_name( + input: &str, +) -> std::result::Result, Box> { let captures = match REGEX.captures(input) { None => return Ok(None), Some(some) => some, }; let resource_name = captures .get(1) - .context(error::ConfigResolutionSnafu { - what: "Resource name could not be extracted from capture.".to_string(), - })? + .ok_or("Resource name could not be extracted from capture.")? .as_str(); let field_name = captures .get(2) - .context(error::ConfigResolutionSnafu { - what: "Resource value could not be extracted from capture.".to_string(), - })? + .ok_or("Resource value could not be extracted from capture.")? .as_str(); Ok(Some((resource_name.to_string(), field_name.to_string()))) } diff --git a/model/src/test_manager/delete.rs b/model/src/test_manager/delete.rs index 771f1e80..9ac8605f 100644 --- a/model/src/test_manager/delete.rs +++ b/model/src/test_manager/delete.rs @@ -48,8 +48,9 @@ impl TestManager { let resources = resource_client .get_all() .await - .context(error::ClientSnafu { - action: "get all resources", + .map_err(|e| error::Error::Client { + action: "get all resources".to_string(), + source: Box::new(e), })?; for resource in resources { topo_sort.insert(CrdName::Resource(resource.name_any())); @@ -63,9 +64,13 @@ impl TestManager { } } let test_client = self.test_client(); - let tests = test_client.get_all().await.context(error::ClientSnafu { - action: "get all tests", - })?; + let tests = test_client + .get_all() + .await + .map_err(|e| error::Error::Client { + action: "get all tests".to_string(), + source: Box::new(e), + })?; for test in tests { if test.spec.resources.is_empty() { topo_sort.insert(CrdName::Test(test.name_any())); @@ -136,8 +141,9 @@ async fn async_deletion( .get(test_name) .await .allow_not_found(|_| ()) - .context(error::ClientSnafu { + .map_err(|e| error::Error::Client { action: format!("get '{}'", test_name), + source: Box::new(e), })?; if test.is_some() { still_awaiting.push(CrdName::Test(test_name.to_string())); @@ -154,8 +160,9 @@ async fn async_deletion( .get(resource_name) .await .allow_not_found(|_| ()) - .context(error::ClientSnafu { + .map_err(|e| error::Error::Client { action: format!("get '{}'", resource_name), + source: Box::new(e), })?; if let Some(resource) = resource { // If the resource errored during deletion alert the user that a problem @@ -201,16 +208,18 @@ async fn async_deletion( .delete(test_name) .await .allow_not_found(|_| ()) - .context(error::ClientSnafu { + .map_err(|e| error::Error::Client { action: format!("delete '{}'", test_name), + source: Box::new(e), }) .map(|_| ()), CrdName::Resource(resource_name) => resource_client .delete(resource_name) .await .allow_not_found(|_| ()) - .context(error::ClientSnafu { + .map_err(|e| error::Error::Client { action: format!("delete '{}'", resource_name), + source: Box::new(e), }) .map(|_| ()), }? diff --git a/model/src/test_manager/error.rs b/model/src/test_manager/error.rs index a37afc4d..6067c078 100644 --- a/model/src/test_manager/error.rs +++ b/model/src/test_manager/error.rs @@ -10,7 +10,7 @@ pub enum Error { #[snafu(display("Unable to {}: {}", action, source))] Client { action: String, - source: crate::clients::Error, + source: Box, }, #[snafu(display("Unable to create client: {}", source))] diff --git a/model/src/test_manager/manager.rs b/model/src/test_manager/manager.rs index 824ae3a4..7d231219 100644 --- a/model/src/test_manager/manager.rs +++ b/model/src/test_manager/manager.rs @@ -156,8 +156,9 @@ impl TestManager { .get_all() .await .allow_not_found(|_| ()) - .context(error::ClientSnafu { - action: "get all resources", + .map_err(|e| error::Error::Client { + action: "get all resources".to_string(), + source: Box::new(e), })? .unwrap_or_default() .is_empty() @@ -175,17 +176,28 @@ impl TestManager { let mut test = test_client .get(name) .await - .context(error::ClientSnafu { action: "get test" })?; + .map_err(|e| error::Error::Client { + action: "get test".to_string(), + source: Box::new(e), + })?; // Created objects are not allowed to have `resource_version` set. test.metadata.resource_version = None; test.status = None; - test_client.delete(name).await.context(error::ClientSnafu { - action: "delete test", - })?; + test_client + .delete(name) + .await + .map_err(|e| error::Error::Client { + action: "delete test".to_string(), + source: Box::new(e), + })?; test_client.wait_for_deletion(name).await; - test_client.create(test).await.context(error::ClientSnafu { - action: "create new test", - })?; + test_client + .create(test) + .await + .map_err(|e| error::Error::Client { + action: "create new test".to_string(), + source: Box::new(e), + })?; Ok(()) } @@ -271,18 +283,21 @@ impl TestManager { for object in objects { match object { Crd::Test(test) => { - self.test_client().delete(test.name_any()).await.context( - error::ClientSnafu { - action: "delete test", - }, - )?; + self.test_client() + .delete(test.name_any()) + .await + .map_err(|e| error::Error::Client { + action: "delete test".to_string(), + source: Box::new(e), + })?; } Crd::Resource(resource) => { self.resource_client() .force_delete(resource.name_any()) .await - .context(error::ClientSnafu { - action: "delete test", + .map_err(|e| error::Error::Client { + action: "delete test".to_string(), + source: Box::new(e), })?; } }; diff --git a/model/src/test_manager/manager_impl.rs b/model/src/test_manager/manager_impl.rs index f8e2eaa7..5a87e7d6 100644 --- a/model/src/test_manager/manager_impl.rs +++ b/model/src/test_manager/manager_impl.rs @@ -100,8 +100,9 @@ impl TestManager { .get(resource) .await .allow_not_found(|_| ()) - .context(error::ClientSnafu { - action: "get resource", + .map_err(|e| error::Error::Client { + action: "get resource".to_string(), + source: Box::new(e), })? { to_be_visited.push(Crd::Resource(resource_spec)); @@ -156,9 +157,13 @@ impl TestManager { /// Add a testsys test to the cluster. pub(super) async fn create_test(&self, test: Test) -> Result<()> { let test_client = self.test_client(); - test_client.create(test).await.context(error::ClientSnafu { - action: "create new test", - })?; + test_client + .create(test) + .await + .map_err(|e| error::Error::Client { + action: "create new test".to_string(), + source: Box::new(e), + })?; Ok(()) } @@ -168,8 +173,9 @@ impl TestManager { resource_client .create(resource) .await - .context(error::ClientSnafu { - action: "create new resource", + .map_err(|e| error::Error::Client { + action: "create new resource".to_string(), + source: Box::new(e), })?; Ok(()) } diff --git a/testsys-launcher/bottlerocket-variant/Cargo.toml b/testsys-launcher/bottlerocket-variant/Cargo.toml new file mode 100644 index 00000000..6224deec --- /dev/null +++ b/testsys-launcher/bottlerocket-variant/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "bottlerocket-variant" +version = "0.1.0" +authors = ["Matt Briggs "] +license = "Apache-2.0 OR MIT" +edition = "2021" +publish = false +# Don't rebuild crate just because of changes to README. +exclude = ["README.md"] + +[dependencies] +serde = "1" +snafu = "0.8" + diff --git a/testsys-launcher/bottlerocket-variant/src/lib.rs b/testsys-launcher/bottlerocket-variant/src/lib.rs new file mode 100644 index 00000000..aa5d4caf --- /dev/null +++ b/testsys-launcher/bottlerocket-variant/src/lib.rs @@ -0,0 +1,442 @@ +/*! +This library provides a structure for representing a Bottlerocket variant as well as functionality +useful in build scripts and other tooling that is variant-aware. +*/ + +use error::Error; +use serde::de::Error as SerdeError; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use snafu::{ensure, OptionExt, ResultExt}; +use std::borrow::Borrow; +use std::convert::TryFrom; +use std::fmt::{Display, Formatter}; +use std::ops::Deref; +use std::str::FromStr; + +/// The name of the environment variable that tells us the current variant. Variant-sensitive crates +/// will need to be rebuilt if this changes. `Makefile.toml` emits the variant string in the +/// `BUILDSYS_VARIANT` environment variable. This is then passed to crate builds by the `Dockerfile` +/// as `VARIANT`. +pub const VARIANT_ENV: &str = "VARIANT"; + +/// The default `variant_version`. If the third position of a variant string tuple does not exist, +/// then the `variant_version` is `"undefined"`. +pub const DEFAULT_VARIANT_VERSION: &str = "0"; + +/// The default `variant_flavor`. If the fourth position of a variant string tuple does not exist, +/// then the variant_flavor cfg will be `"none"`. +pub const DEFAULT_VARIANT_FLAVOR: &str = "none"; + +pub type Result = std::result::Result; + +pub mod error { + use snafu::Snafu; + + #[derive(Debug, Snafu)] + #[snafu(visibility(pub(super)))] + pub enum Error { + #[snafu(display( + "The 'VARIANT' environment variable is missing or unable to be read: {}", + source + ))] + VariantEnv { source: std::env::VarError }, + + #[snafu(display("The '{}' segment of the variant '{}' is missing", part_name, variant))] + VariantPart { part_name: String, variant: String }, + + #[snafu(display("The '{}' segment of the variant '{}' is empty", part_name, variant))] + VariantPartEmpty { part_name: String, variant: String }, + } +} + +/// # Variant +/// +/// Represents a Bottlerocket variant string. These are in the form +/// `platform-runtime-[variant_version][-variant_flavor]`. +/// +/// For example, here are some valid variant strings: +/// - aws-ecs-1 +/// - vmware-k8s-1.32 +/// - metal-dev +/// - aws-k8s-1.32-nvidia +/// +/// The `platform` and `runtime` values are required. `variant_version` and `variant_flavor` values +/// are optional and will default to `"0"` and `"none"` respectively. +/// +/// In a `build.rs` file, you may use the function `emit_cfgs()` if you need to conditionally +/// compile code based on variant characteristics. +/// +/// # Example +/// +/// ```rust +/// use bottlerocket_variant::{Variant, VARIANT_ENV}; +/// std::env::set_var(VARIANT_ENV, "vmware-k8s-1.32"); +/// let variant = Variant::from_env().unwrap(); +/// +/// assert_eq!(variant.version().unwrap(), "1.32"); +/// +/// // In a `build.rs` file, you may want to emit cfgs that you can use for conditional compilation. +/// variant.emit_cfgs(); +/// ``` +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Variant { + variant: String, + platform: String, + runtime: String, + family: String, + version: Option, + variant_flavor: Option, +} + +impl Variant { + /// Create a new `Variant` from a dash-delimited string. The first two tuple positions, + /// `platform` and `runtime` are required. The next two, representing `variant_version` and + /// `variant_flavor`, are optional. + /// + /// # Valid Values + /// + /// - `aws-dev` + /// - `vmware-k8s-1.32` + /// - `aws-k8s-1.32-nvidia` + /// - `aws-k8s-1.32-nvidia-some-additional-ignored-tuple-positions` + /// + /// # Invalid Values + /// + /// - `aws` + /// - `aws-dev-` + /// + /// # Example + /// + /// ```rust + /// use bottlerocket_variant::Variant; + /// let variant = Variant::new("aws-k8s").unwrap(); + /// assert_eq!(variant.family(), "aws-k8s"); + /// ``` + pub fn new>(value: S) -> Result { + Self::parse(value) + } + + /// Create a new `Variant` from the `VARIANT` environment variable's value. The environment + /// variable must exist and its value must be a valid variant string tuple. + pub fn from_env() -> Result { + let value = std::env::var(VARIANT_ENV).context(error::VariantEnvSnafu)?; + Variant::new(value) + } + + /// The variant's platform. This is the first member of the tuple. For example, in `vmware-dev`, + /// `vmware` is the platform. + pub fn platform(&self) -> &str { + &self.platform + } + + /// The variant's runtime. This is the second member of the tuple. For example, in + /// `vmware-k8s-1.32`, `k8s` is the `runtime`. + pub fn runtime(&self) -> &str { + &self.runtime + } + + /// The variant's family. This is the `platform` and `runtime` together. For example, in + /// `aws-k8s-1.32`, `aws-k8s` is the `family`. + pub fn family(&self) -> &str { + &self.family + } + + /// The variant's version. This is the optional third value in the variant string tuple. For + /// example for `aws-ecs-1` the `version` is `1`. If the `version` does not exist, + /// [`DEFAULT_VARIANT_VERSION`] is returned. + pub fn version(&self) -> Option<&str> { + self.version.as_deref() + } + + /// The variant's flavor. This is the optional fourth value in the variant string tuple. For + /// example for `aws-k8s-1.32-nvidia` the `variant_flavor` is `nvidia`. + pub fn variant_flavor(&self) -> Option<&str> { + self.variant_flavor.as_deref() + } + + /// This can be used in a `build.rs` file to tell cargo that the crate needs to be rebuilt if + /// the variant changes. + pub fn rerun_if_changed() { + println!("cargo:rerun-if-env-changed={VARIANT_ENV}"); + } + + /// This can be used in a `build.rs` file to emit `cfg` values that can be used for conditional + /// compilation based on variant characteristics. This function also emits rerun-if-changed so + /// that variant-sensitive builds will rebuild if the variant changes. + /// + /// # Example + /// + /// Given a variant `aws-k8s-1.32`, if this function has been called in `build.rs`, then + /// all of the following conditional complition checks would evaluate to `true`. + /// + /// `#[cfg(variant = "aws-k8s-1.32")]` + /// `#[cfg(variant_platform = "aws")]` + /// `#[cfg(variant_runtime = "k8s")]` + /// `#[cfg(variant_family = "aws-k8s")]` + /// `#[cfg(variant_version = "1.32")]` + /// `#[cfg(variant_flavor = "none")]` + pub fn emit_cfgs(&self) { + Self::rerun_if_changed(); + println!("cargo:rustc-cfg=variant=\"{self}\""); + println!("cargo:rustc-cfg=variant_platform=\"{}\"", self.platform()); + println!("cargo:rustc-cfg=variant_runtime=\"{}\"", self.runtime()); + println!("cargo:rustc-cfg=variant_family=\"{}\"", self.family()); + println!( + "cargo:rustc-cfg=variant_version=\"{}\"", + self.version().unwrap_or(DEFAULT_VARIANT_VERSION) + ); + println!( + "cargo:rustc-cfg=variant_flavor=\"{}\"", + self.variant_flavor().unwrap_or(DEFAULT_VARIANT_FLAVOR) + ); + } + + fn parse>(value: S) -> Result { + let variant = value.into(); + let mut parts = variant.split('-'); + let platform = parts + .next() + .with_context(|| error::VariantPartSnafu { + part_name: "platform", + variant: variant.clone(), + })? + .to_string(); + ensure!( + !platform.is_empty(), + error::VariantPartEmptySnafu { + part_name: "platform", + variant: variant.clone() + } + ); + let runtime = parts + .next() + .with_context(|| error::VariantPartSnafu { + part_name: "runtime", + variant: variant.clone(), + })? + .to_string(); + ensure!( + !runtime.is_empty(), + error::VariantPartEmptySnafu { + part_name: "runtime", + variant: variant.clone() + } + ); + let variant_family = format!("{platform}-{runtime}"); + let variant_version = parts.next().map(|s| s.to_string()); + if let Some(value) = variant_version.as_ref() { + ensure!( + !value.is_empty(), + error::VariantPartEmptySnafu { + part_name: "variant_version", + variant: variant.clone() + } + ); + } + let variant_flavor = parts.next().map(|s| s.to_string()); + if let Some(value) = variant_flavor.as_ref() { + ensure!( + !value.is_empty(), + error::VariantPartEmptySnafu { + part_name: "variant_flavor", + variant: variant.clone() + } + ); + } + Ok(Self { + variant, + platform, + runtime, + family: variant_family, + version: variant_version, + variant_flavor, + }) + } +} + +impl FromStr for Variant { + type Err = Error; + + fn from_str(s: &str) -> Result { + Variant::new(s) + } +} + +impl TryFrom for Variant { + type Error = Error; + + fn try_from(value: String) -> std::result::Result { + Variant::new(value) + } +} + +impl TryFrom<&str> for Variant { + type Error = Error; + + fn try_from(value: &str) -> std::result::Result { + Variant::new(value) + } +} + +impl Serialize for Variant { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(&self.variant) + } +} + +impl<'de> Deserialize<'de> for Variant { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + Variant::new(value).map_err(|e| D::Error::custom(format!("Error parsing variant: {e}"))) + } +} + +impl Deref for Variant { + type Target = str; + fn deref(&self) -> &Self::Target { + &self.variant + } +} + +impl Borrow for Variant { + fn borrow(&self) -> &String { + &self.variant + } +} + +impl Borrow for Variant { + fn borrow(&self) -> &str { + &self.variant + } +} + +impl AsRef for Variant { + fn as_ref(&self) -> &str { + &self.variant + } +} + +impl Display for Variant { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.variant, f) + } +} + +impl From for String { + fn from(x: Variant) -> Self { + x.variant + } +} + +impl PartialEq for Variant { + fn eq(&self, other: &str) -> bool { + self.variant == other + } +} + +impl PartialEq for Variant { + fn eq(&self, other: &String) -> bool { + &self.variant == other + } +} + +impl PartialEq<&str> for Variant { + fn eq(&self, other: &&str) -> bool { + &self.variant == other + } +} + +impl PartialEq for str { + fn eq(&self, other: &Variant) -> bool { + self == other.variant + } +} + +impl PartialEq for String { + fn eq(&self, other: &Variant) -> bool { + self.as_str() == other + } +} + +impl PartialEq for &str { + fn eq(&self, other: &Variant) -> bool { + self == &other.variant + } +} + +#[test] +fn parse_ok() { + struct Test { + input: &'static str, + platform: &'static str, + runtime: &'static str, + variant_family: &'static str, + variant_version: Option<&'static str>, + variant_flavor: Option<&'static str>, + } + + let tests = vec![ + Test { + input: "aws-k8s-1.21", + platform: "aws", + runtime: "k8s", + variant_family: "aws-k8s", + variant_version: Some("1.21"), + variant_flavor: None, + }, + Test { + input: "metal-dev", + platform: "metal", + runtime: "dev", + variant_family: "metal-dev", + variant_version: None, + variant_flavor: None, + }, + Test { + input: "aws-ecs-1", + platform: "aws", + runtime: "ecs", + variant_family: "aws-ecs", + variant_version: Some("1"), + variant_flavor: None, + }, + Test { + input: "aws-k8s-1.32-nvidia-some-additional-ignored-tuple-positions", + platform: "aws", + runtime: "k8s", + variant_family: "aws-k8s", + variant_version: Some("1.32"), + variant_flavor: Some("nvidia"), + }, + ]; + + for test in tests { + let parsed = Variant::new(test.input).unwrap(); + assert_eq!(parsed, test.input); + assert_eq!(test.input, parsed); + assert_eq!(parsed.platform(), test.platform.to_string()); + assert_eq!(parsed.runtime(), test.runtime); + assert_eq!(parsed.family(), test.variant_family); + assert_eq!(parsed.version(), test.variant_version); + assert_eq!(parsed.variant_flavor(), test.variant_flavor); + } +} + +#[test] +fn parse_err() { + let tests = vec!["aws", "aws-", "aws-dev-", "aws-k8s-1.32-"]; + for test in tests { + let result = Variant::new(test); + assert!( + result.is_err(), + "Expected Variant::new(\"{}\") to return an error", + test + ); + } +} diff --git a/testsys-launcher/parse-datetime/Cargo.toml b/testsys-launcher/parse-datetime/Cargo.toml new file mode 100644 index 00000000..0ef8669b --- /dev/null +++ b/testsys-launcher/parse-datetime/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "parse-datetime" +version = "0.1.0" +authors = ["Zac Mrowicki "] +license = "Apache-2.0 OR MIT" +edition = "2021" +publish = false +# Don't rebuild crate just because of changes to README. +exclude = ["README.md"] + +[dependencies] +chrono = { version = "0.4", features = ["clock", "std"] } +snafu = { version = "0.8", features = ["backtraces-impl-backtrace-crate"] } diff --git a/testsys-launcher/parse-datetime/src/lib.rs b/testsys-launcher/parse-datetime/src/lib.rs new file mode 100644 index 00000000..698bc007 --- /dev/null +++ b/testsys-launcher/parse-datetime/src/lib.rs @@ -0,0 +1,164 @@ +/*! +# Background + +This library parses a `DateTime` from a string. + +The string can be: + +* an `RFC3339` formatted date / time +* a string with the form `"[in] "` where 'in' is optional + * `` may be any unsigned integer and + * `` may be either the singular or plural form of the following: `hour | hours`, `day | days`, `week | weeks` + +Examples: + +* `"in 1 hour"` +* `"in 2 hours"` +* `"in 6 days"` +* `"in 2 weeks"` +* `"1 hour"` +* `"7 days"` +*/ + +use chrono::{DateTime, Duration, FixedOffset, Utc}; +use snafu::{ensure, OptionExt, ResultExt}; + +mod error { + use snafu::Snafu; + + #[derive(Debug, Snafu)] + #[snafu(visibility(pub(super)))] + pub enum Error { + #[snafu(display("Date argument '{}' is invalid: {}", input, msg))] + DateArgInvalid { input: String, msg: &'static str }, + + #[snafu(display( + "Date argument had count '{}' that failed to parse as integer: {}", + input, + source + ))] + DateArgCount { + input: String, + source: std::num::ParseIntError, + }, + + #[snafu(display("Integer '{}' is not convertable to a number of {}", integer, unit))] + DateInt { integer: u64, unit: &'static str }, + } +} +pub use error::Error; +type Result = std::result::Result; + +/// Parses a user-specified datetime, either in full RFC 3339 format, or a shorthand like "in 7 +/// days" that's taken as an offset from the time the function is run. +pub fn parse_datetime(input: &str) -> Result> { + // If the user gave an absolute date in a standard format, accept it. + let try_dt: std::result::Result, chrono::format::ParseError> = + DateTime::parse_from_rfc3339(input); + if let Ok(dt) = try_dt { + let utc = dt.into(); + return Ok(utc); + } + + let offset = parse_offset(input)?; + + let now = Utc::now(); + let then = now + offset; + Ok(then) +} + +/// Parses a user-specified datetime offset in the form of a shorthand like "in 7 days". +pub fn parse_offset(input: &str) -> Result { + // Otherwise, pull apart a request like "in 5 days" to get an exact datetime. + let mut parts: Vec<&str> = input.split_whitespace().collect(); + ensure!( + parts.len() == 3 || parts.len() == 2, + error::DateArgInvalidSnafu { + input, + msg: "expected RFC 3339, or something like 'in 7 days' or '7 days'" + } + ); + let unit_str = parts.pop().unwrap(); + let count_str = parts.pop().unwrap(); + + // the prefix string 'in' is optional + if let Some(prefix_str) = parts.pop() { + ensure!( + prefix_str == "in", + error::DateArgInvalidSnafu { + input, + msg: "expected prefix 'in', something like 'in 7 days'", + } + ); + } + + let count: u32 = count_str + .parse() + .context(error::DateArgCountSnafu { input })?; + + let duration = match unit_str { + "minute" | "minutes" => { + Duration::try_minutes(i64::from(count)).context(error::DateIntSnafu { + integer: count, + unit: "minutes", + })? + } + "hour" | "hours" => Duration::try_hours(i64::from(count)).context(error::DateIntSnafu { + integer: count, + unit: "hours", + })?, + "day" | "days" => Duration::try_days(i64::from(count)).context(error::DateIntSnafu { + integer: count, + unit: "days", + })?, + "week" | "weeks" => Duration::try_weeks(i64::from(count)).context(error::DateIntSnafu { + integer: count, + unit: "weeks", + })?, + _ => { + return error::DateArgInvalidSnafu { + input, + msg: "date argument's unit must be minutes/hours/days/weeks", + } + .fail(); + } + }; + + Ok(duration) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_acceptable_strings() { + let inputs = vec![ + "in 0 hours", + "in 1 hour", + "in 5000000 hours", + "in 0 days", + "in 1 day", + "in 5000000 days", + "in 0 weeks", + "in 1 week", + "in 5000000 weeks", + "0 weeks", + "1 week", + "5000000 weeks", + ]; + + for input in inputs { + assert!(parse_datetime(input).is_ok()) + } + } + + #[test] + fn test_unacceptable_strings() { + let inputs = vec!["in", "0 hou", "hours", "in 1 month"]; + + for input in inputs { + assert!(parse_datetime(input).is_err()) + } + } +} diff --git a/testsys-launcher/pubsys-config/2w-2w-1w.toml b/testsys-launcher/pubsys-config/2w-2w-1w.toml new file mode 100644 index 00000000..61847d28 --- /dev/null +++ b/testsys-launcher/pubsys-config/2w-2w-1w.toml @@ -0,0 +1,3 @@ +snapshot_expiration = '2 weeks' +targets_expiration = '2 weeks' +timestamp_expiration = '1 week' \ No newline at end of file diff --git a/testsys-launcher/pubsys-config/Cargo.toml b/testsys-launcher/pubsys-config/Cargo.toml new file mode 100644 index 00000000..6eef0afe --- /dev/null +++ b/testsys-launcher/pubsys-config/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "pubsys-config" +version = "0.1.0" +authors = ["Zac Mrowicki ", "Tom Kirchner "] +license = "Apache-2.0 OR MIT" +edition = "2021" +publish = false + +[dependencies] +chrono = { version = "0.4", features = ["clock", "std"] } +home = "0.5" +lazy_static = "1" +log = "0.4" +parse-datetime = { version = "0.1", path = "../parse-datetime" } +serde = { version = "1", features = ["derive"] } +serde_yaml = "0.9" +snafu = "0.8" +toml = "0.8" +url = { version = "2", features = ["serde"] } diff --git a/testsys-launcher/pubsys-config/src/lib.rs b/testsys-launcher/pubsys-config/src/lib.rs new file mode 100644 index 00000000..7d873a3c --- /dev/null +++ b/testsys-launcher/pubsys-config/src/lib.rs @@ -0,0 +1,300 @@ +//! The config module owns the definition and loading process for our configuration sources. +pub mod vmware; + +use crate::vmware::VmwareConfig; +use chrono::Duration; +use log::info; +use parse_datetime::parse_offset; +use serde::{Deserialize, Deserializer, Serialize}; +use snafu::{OptionExt, ResultExt}; +use std::collections::{HashMap, VecDeque}; +use std::convert::TryFrom; +use std::fs; +use std::num::NonZeroUsize; +use std::path::{Path, PathBuf}; +use url::Url; + +/// Configuration needed to load and create repos +#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct InfraConfig { + // Repo subcommand config + pub repo: Option>, + + // Config for AWS specific subcommands + pub aws: Option, + + // Config for VMware specific subcommands + pub vmware: Option, + + // Config for container registries + pub vendor: Option>, +} + +impl InfraConfig { + /// Deserializes an InfraConfig from a given path + pub fn from_path

(path: P) -> Result + where + P: AsRef, + { + let path = path.as_ref(); + let infra_config_str = fs::read_to_string(path).context(error::FileSnafu { path })?; + toml::from_str(&infra_config_str).context(error::InvalidTomlSnafu { path }) + } + + /// Deserializes an InfraConfig from a Infra.lock file at a given path + pub fn from_lock_path

(path: P) -> Result + where + P: AsRef, + { + let path = path.as_ref(); + let infra_config_str = fs::read_to_string(path).context(error::FileSnafu { path })?; + serde_yaml::from_str(&infra_config_str).context(error::InvalidLockSnafu { path }) + } + + /// Deserializes an InfraConfig from a given path, if it exists, otherwise builds a default + /// config + pub fn from_path_or_default

(path: P) -> Result + where + P: AsRef, + { + if path.as_ref().exists() { + Self::from_path(path) + } else { + Ok(Self::default()) + } + } + + /// Deserializes an InfraConfig from Infra.lock, if it exists, otherwise uses Infra.toml + /// If the default flag is true, will create a default config if Infra.toml doesn't exist + pub fn from_path_or_lock(path: &Path, default: bool) -> Result { + let lock_path = Self::compute_lock_path(path)?; + if lock_path.exists() { + info!("Found infra config at path: {}", lock_path.display()); + Self::from_lock_path(lock_path) + } else if default { + Self::from_path_or_default(path) + } else { + info!("Found infra config at path: {}", path.display()); + Self::from_path(path) + } + } + + /// Looks for a file named `Infra.lock` in the same directory as the file named by + /// `infra_config_path`. Returns true if the `Infra.lock` file exists, or if `infra_config_path` + /// exists. Returns an error if the directory of `infra_config_path` cannot be found. + pub fn lock_or_infra_config_exists

(infra_config_path: P) -> Result + where + P: AsRef, + { + let lock_path = Self::compute_lock_path(&infra_config_path)?; + Ok(lock_path.exists() || infra_config_path.as_ref().exists()) + } + + /// Returns the file path to a file named `Infra.lock` in the same directory as the file named + /// by `infra_config_path`. + pub fn compute_lock_path

(infra_config_path: P) -> Result + where + P: AsRef, + { + Ok(infra_config_path + .as_ref() + .parent() + .context(error::ParentSnafu { + path: infra_config_path.as_ref(), + })? + .join("Infra.lock")) + } +} + +/// Container registry vendor +#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq, Clone)] +pub struct Vendor { + pub registry: String, +} + +/// S3-specific TUF infrastructure configuration +#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq, Clone)] +pub struct S3Config { + pub region: Option, + #[serde(default)] + pub s3_prefix: String, + pub vpc_endpoint_id: Option, + pub stack_arn: Option, + pub bucket_name: Option, +} + +/// AWS-specific infrastructure configuration +#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq, Clone)] +#[serde(deny_unknown_fields)] +pub struct AwsConfig { + #[serde(default)] + pub regions: VecDeque, + pub role: Option, + pub profile: Option, + #[serde(default)] + pub region: HashMap, + pub ssm_prefix: Option, + pub s3: Option>, +} + +/// AWS region-specific configuration +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] +#[serde(deny_unknown_fields)] +pub struct AwsRegionConfig { + pub role: Option, +} + +/// Location of signing keys +// These variant names are lowercase because they have to match the text in Infra.toml, and it's +// more common for TOML config to be lowercase. +#[allow(non_camel_case_types)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub enum SigningKeyConfig { + file { + path: PathBuf, + }, + kms { + key_id: Option, + #[serde(flatten)] + config: Option, + }, + ssm { + parameter: String, + }, + env { + var_name: String, + }, +} + +/// AWS region-specific configuration +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +//#[serde(deny_unknown_fields)] +pub struct KMSKeyConfig { + #[serde(default)] + pub available_keys: HashMap, + pub key_alias: Option, + #[serde(default)] + pub regions: VecDeque, + #[serde(default)] + pub key_stack_arns: HashMap, +} + +impl TryFrom for Url { + type Error = (); + fn try_from(key: SigningKeyConfig) -> std::result::Result { + match key { + SigningKeyConfig::file { path } => Url::from_file_path(path), + // We don't support passing profiles to tough in the name of the key/parameter, so for + // KMS and SSM we prepend a slash if there isn't one present. + SigningKeyConfig::kms { key_id, .. } => { + let mut key_id = key_id.unwrap_or_default(); + key_id = if key_id.starts_with('/') { + key_id.to_string() + } else { + format!("/{key_id}") + }; + Url::parse(&format!("aws-kms://{key_id}")).map_err(|_| ()) + } + SigningKeyConfig::ssm { parameter } => { + let parameter = if parameter.starts_with('/') { + parameter + } else { + format!("/{parameter}") + }; + Url::parse(&format!("aws-ssm://{parameter}")).map_err(|_| ()) + } + SigningKeyConfig::env { var_name } => { + Url::parse(&format!("env://{var_name}")).map_err(|_| ()) + } + } + } +} + +/// Represents a Bottlerocket repo's location and the metadata needed to update the repo +#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct RepoConfig { + pub root_role_url: Option, + pub root_role_sha512: Option, + pub signing_keys: Option, + pub root_keys: Option, + pub metadata_base_url: Option, + pub targets_url: Option, + pub file_hosting_config_name: Option, + pub root_key_threshold: Option, + pub pub_key_threshold: Option, +} + +/// How long it takes for each metadata type to expire +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RepoExpirationPolicy { + #[serde(deserialize_with = "deserialize_offset")] + pub snapshot_expiration: Duration, + #[serde(deserialize_with = "deserialize_offset")] + pub targets_expiration: Duration, + #[serde(deserialize_with = "deserialize_offset")] + pub timestamp_expiration: Duration, +} + +impl RepoExpirationPolicy { + /// Deserializes a RepoExpirationPolicy from a given path + pub fn from_path

(path: P) -> Result + where + P: AsRef, + { + let path = path.as_ref(); + let expiration_str = fs::read_to_string(path).context(error::FileSnafu { path })?; + toml::from_str(&expiration_str).context(error::InvalidTomlSnafu { path }) + } +} + +/// Deserializes a Duration in the form of "in X hours/days/weeks" +fn deserialize_offset<'de, D>(deserializer: D) -> std::result::Result +where + D: Deserializer<'de>, +{ + let s: String = Deserialize::deserialize(deserializer)?; + parse_offset(&s).map_err(serde::de::Error::custom) +} + +mod error { + use snafu::Snafu; + use std::io; + use std::path::PathBuf; + + #[derive(Debug, Snafu)] + #[snafu(visibility(pub(super)))] + pub enum Error { + #[snafu(display("Failed to read '{}': {}", path.display(), source))] + File { path: PathBuf, source: io::Error }, + + #[snafu(display("Invalid config file at '{}': {}", path.display(), source))] + InvalidToml { + path: PathBuf, + source: toml::de::Error, + }, + + #[snafu(display("Invalid lock file at '{}': {}", path.display(), source))] + InvalidLock { + path: PathBuf, + source: serde_yaml::Error, + }, + + #[snafu(display("Missing config: {}", what))] + MissingConfig { what: String }, + + #[snafu(display("Failed to get parent of path: {}", path.display()))] + Parent { path: PathBuf }, + } +} +pub use error::Error; +pub type Result = std::result::Result; + +#[test] +fn repo_expiration_deserialization_test() { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("2w-2w-1w.toml"); + let _ = RepoExpirationPolicy::from_path(path).unwrap(); +} diff --git a/testsys-launcher/pubsys-config/src/vmware.rs b/testsys-launcher/pubsys-config/src/vmware.rs new file mode 100644 index 00000000..6202726d --- /dev/null +++ b/testsys-launcher/pubsys-config/src/vmware.rs @@ -0,0 +1,221 @@ +//! The vmware module owns the definition and loading process for our VMware configuration sources. +use lazy_static::lazy_static; +use log::debug; +use serde::{Deserialize, Serialize}; +use snafu::{OptionExt, ResultExt}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::{env, fs}; + +lazy_static! { + /// Determine the full path to the Vsphere credentials at runtime. This is an Option because it is + /// possible (however unlikely) that `home_dir()` is unable to find the home directory of the + /// current user + pub static ref VMWARE_CREDS_PATH: Option = home::home_dir().map(|home| home + .join(".config") + .join("pubsys") + .join("vsphere-credentials.toml")); +} + +const GOVC_USERNAME: &str = "GOVC_USERNAME"; +const GOVC_PASSWORD: &str = "GOVC_PASSWORD"; +const GOVC_URL: &str = "GOVC_URL"; +const GOVC_DATACENTER: &str = "GOVC_DATACENTER"; +const GOVC_DATASTORE: &str = "GOVC_DATASTORE"; +const GOVC_NETWORK: &str = "GOVC_NETWORK"; +const GOVC_RESOURCE_POOL: &str = "GOVC_RESOURCE_POOL"; +const GOVC_FOLDER: &str = "GOVC_FOLDER"; + +/// VMware-specific infrastructure configuration +#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct VmwareConfig { + #[serde(default)] + pub datacenters: Vec, + #[serde(default)] + pub datacenter: HashMap, + pub common: Option, +} + +/// VMware datacenter-specific configuration. +/// +/// Fields are optional here because this struct is used to gather environment variables, common +/// config, and datacenter-specific configuration, each of which may not have the complete set of +/// fields. It is used to build a complete datacenter configuration (hence the "Builder" name). +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct DatacenterBuilder { + pub vsphere_url: Option, + pub datacenter: Option, + pub datastore: Option, + pub network: Option, + pub folder: Option, + pub resource_pool: Option, +} + +/// Helper macro for retrieving a field from another struct if the field in `self` is `None` +macro_rules! field_or { + ($self:expr, $field:ident, $other:expr) => { + $self + .$field + .as_ref() + .or($other.and_then(|o| o.$field.as_ref())) + .cloned() + }; +} + +impl DatacenterBuilder { + /// Create a DatacenterBuilder from environment variables + pub fn from_env() -> Self { + Self { + vsphere_url: get_env(GOVC_URL), + datacenter: get_env(GOVC_DATACENTER), + datastore: get_env(GOVC_DATASTORE), + network: get_env(GOVC_NETWORK), + folder: get_env(GOVC_FOLDER), + resource_pool: get_env(GOVC_RESOURCE_POOL), + } + } + + /// Creates a new DatacenterBuilder, merging fields from another (Optional) + /// DatacenterBuilder if the field in `self` is None + pub fn take_missing_from(&self, other: Option<&Self>) -> Self { + Self { + vsphere_url: field_or!(self, vsphere_url, other), + datacenter: field_or!(self, datacenter, other), + datastore: field_or!(self, datastore, other), + network: field_or!(self, network, other), + folder: field_or!(self, folder, other), + resource_pool: field_or!(self, resource_pool, other), + } + } + + /// Attempts to create a `Datacenter`, consuming `self` and ensuring that each field contains a + /// value. + pub fn build(self) -> Result { + let get_or_err = + |opt: Option, what: &str| opt.context(error::MissingConfigSnafu { what }); + + Ok(Datacenter { + vsphere_url: get_or_err(self.vsphere_url, "vSphere URL")?, + datacenter: get_or_err(self.datacenter, "vSphere datacenter")?, + datastore: get_or_err(self.datastore, "vSphere datastore")?, + network: get_or_err(self.network, "vSphere network")?, + folder: get_or_err(self.folder, "vSphere folder")?, + resource_pool: get_or_err(self.resource_pool, "vSphere resource pool")?, + }) + } +} + +/// A fully configured VMware datacenter, i.e. no optional fields +#[derive(Debug)] +pub struct Datacenter { + pub vsphere_url: String, + pub datacenter: String, + pub datastore: String, + pub network: String, + pub folder: String, + pub resource_pool: String, +} + +/// VMware infrastructure credentials for all datacenters +#[derive(Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DatacenterCredsConfig { + #[serde(default)] + pub datacenter: HashMap, +} + +impl DatacenterCredsConfig { + /// Deserializes a DatacenterCredsConfig from a given path + pub fn from_path

(path: P) -> Result + where + P: AsRef, + { + let path = path.as_ref(); + let creds_config_str = fs::read_to_string(path).context(error::FileSnafu { path })?; + toml::from_str(&creds_config_str).context(error::InvalidTomlSnafu { path }) + } +} + +/// VMware datacenter-specific credentials. Fields are optional here since this struct is used to +/// gather environment variables as well as fields from file, either of which may or may not exist. +/// It is used to build a complete credentials configuration (hence the "Builder" name). +#[derive(Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DatacenterCredsBuilder { + pub username: Option, + pub password: Option, +} + +impl DatacenterCredsBuilder { + /// Create a DatacenterCredsBuilder from environment variables + pub fn from_env() -> Self { + Self { + username: get_env(GOVC_USERNAME), + password: get_env(GOVC_PASSWORD), + } + } + + /// Creates a new DatacenterCredsBuilder, merging fields from another (Optional) + /// DatacenterCredsBuilder if the field in `self` is None + pub fn take_missing_from(&self, other: Option<&Self>) -> Self { + Self { + username: field_or!(self, username, other), + password: field_or!(self, password, other), + } + } + /// Attempts to create a `DatacenterCreds`, consuming `self` and ensuring that each field + /// contains a value + pub fn build(self) -> Result { + let get_or_err = + |opt: Option, what: &str| opt.context(error::MissingConfigSnafu { what }); + + Ok(DatacenterCreds { + username: get_or_err(self.username, "vSphere username")?, + password: get_or_err(self.password, "vSphere password")?, + }) + } +} + +/// Fully configured datacenter credentials, i.e. no optional fields +#[derive(Debug)] +pub struct DatacenterCreds { + pub username: String, + pub password: String, +} + +/// Attempt to retrieve an environment variable, returning None if it doesn't exist +fn get_env(var: &str) -> Option { + match env::var(var) { + Ok(v) => Some(v), + Err(e) => { + debug!("Unable to read environment variable '{var}': {e}"); + None + } + } +} + +mod error { + use snafu::Snafu; + use std::io; + use std::path::PathBuf; + + #[derive(Debug, Snafu)] + #[snafu(visibility(pub(super)))] + pub enum Error { + #[snafu(display("Failed to read '{}': {}", path.display(), source))] + File { path: PathBuf, source: io::Error }, + + #[snafu(display("Invalid config file at '{}': {}", path.display(), source))] + InvalidToml { + path: PathBuf, + source: toml::de::Error, + }, + + #[snafu(display("Missing config: {}", what))] + MissingConfig { what: String }, + } +} +pub use error::Error; +pub type Result = std::result::Result; diff --git a/testsys-launcher/testsys-config/Cargo.toml b/testsys-launcher/testsys-config/Cargo.toml new file mode 100644 index 00000000..0e28556c --- /dev/null +++ b/testsys-launcher/testsys-config/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "testsys-config" +version = "0.1.0" +authors = ["Ethan Pullen "] +license = "Apache-2.0 OR MIT" +edition = "2021" +publish = false + +[dependencies] +bottlerocket-types = { version = "0.0.17", path = "../../bottlerocket/types" } +bottlerocket-variant = { version = "0.1", path = "../bottlerocket-variant" } +handlebars = "5" +log = "0.4" +maplit = "1" +testsys-model = { version = "0.0.17", path = "../../model" } +serde = "1" +serde_plain = "1" +serde_yaml = "0.9" +snafu = "0.8" +toml = "0.8" diff --git a/testsys-launcher/testsys-config/src/lib.rs b/testsys-launcher/testsys-config/src/lib.rs new file mode 100644 index 00000000..5cff72a8 --- /dev/null +++ b/testsys-launcher/testsys-config/src/lib.rs @@ -0,0 +1,550 @@ +use bottlerocket_types::agent_config::BlockDeviceMappingConfig; +use bottlerocket_variant::Variant; +pub use error::Error; +use handlebars::Handlebars; +use log::{debug, trace, warn}; +use maplit::btreemap; +use serde::{Deserialize, Serialize}; +use snafu::ResultExt; +use std::collections::{BTreeMap, HashMap}; +use std::fs; +use std::path::Path; +use testsys_model::constants::TESTSYS_VERSION; +use testsys_model::{DestructionPolicy, SecretName}; +pub type Result = std::result::Result; +use serde_plain::derive_fromstr_from_deserialize; + +/// Configuration needed to run tests +#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct TestConfig { + /// High level configuration for TestSys + pub test: Option, + + #[serde(flatten)] + /// Configuration for testing variants + pub configs: HashMap, +} + +impl TestConfig { + /// Deserializes a TestConfig from a given path + pub fn from_path

(path: P) -> Result + where + P: AsRef, + { + let path = path.as_ref(); + let test_config_str = fs::read_to_string(path).context(error::FileSnafu { path })?; + let mut config: Self = + toml::from_str(&test_config_str).context(error::InvalidTomlSnafu { path })?; + // Copy the GenericConfig from `test` to `configs`. + config.test.as_ref().and_then(|test| { + config + .configs + .insert("test".to_string(), test.config.clone()) + }); + + Ok(config) + } + + /// Deserializes a TestConfig from a given path, if it exists, otherwise builds a default + /// config + pub fn from_path_or_default

(path: P) -> Result + where + P: AsRef, + { + if path.as_ref().exists() { + Self::from_path(path) + } else { + warn!( + "No test config was found at '{}'. Using the default config.", + path.as_ref().display() + ); + Ok(Self::default()) + } + } + + /// Create a single config for the `variant` and `arch` from this test configuration by + /// determining a list of tables that contain information relevant to the arch, variant + /// combination. Then, the tables are reduced to a single config by selecting values from the + /// table based on the order of precedence. If `starting_config` is provided it will be used as + /// the config with the highest precedence. + pub fn reduced_config( + &self, + variant: &Variant, + arch: S, + starting_config: Option, + test_type: &str, + ) -> (GenericVariantConfig, String) + where + S: Into, + { + let arch = arch.into(); + // Starting with a list of keys ordered by precedence, return a single config with values + // selected by the order of the list. + let (test_type, configs) = config_keys(variant) + // Convert the vec of keys in to an iterator of keys. + .into_iter() + // Convert the iterator of keys to and iterator of Configs. If the key does not have a + // configuration in the config file, remove it from the iterator. + .filter_map(|key| self.configs.get(&key).cloned()) + // Reverse the iterator + .rev() + .fold( + (test_type.to_string(), Vec::new()), + |(test_type, mut configs), config| { + let (ordered_configs, test_type) = config.test_configs(test_type); + configs.push(ordered_configs); + (test_type, configs) + }, + ); + debug!("Resolved test-type '{test_type}'"); + ( + configs + .into_iter() + .rev() + .flatten() + // Take the iterator of configurations and extract the arch specific config and the + // non-arch specific config for each config. Then, convert them into a single iterator. + .flat_map(|config| vec![config.for_arch(&arch), config.config]) + // Take the iterator of configurations and merge them into a single config by populating + // each field with the first value that is not `None` while following the list of + // precedence. + .fold( + starting_config.unwrap_or_default(), + GenericVariantConfig::merge, + ), + test_type, + ) + } +} + +/// High level configurations for a test +#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq, Clone)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +pub struct Test { + /// The name of the repo in `Infra.toml` that should be used for testing + pub repo: Option, + + /// The name of the vSphere data center in `Infra.toml` that should be used for testing + /// If no data center is provided, the first one in `vmware.datacenters` will be used + pub datacenter: Option, + + #[serde(flatten)] + /// The URI of TestSys images + pub testsys_images: TestsysImages, + + /// A registry containing all TestSys images + pub testsys_image_registry: Option, + + /// The tag that should be used for TestSys images + pub testsys_image_tag: Option, + + #[serde(flatten)] + /// Configuration values for all Bottlerocket variants + pub config: GenericConfig, +} + +/// Create a vec of relevant keys for this variant ordered from most specific to least specific. +fn config_keys(variant: &Variant) -> Vec { + let (family_flavor, platform_flavor) = variant + .variant_flavor() + .map(|flavor| { + ( + format!("{}-{}", variant.family(), flavor), + format!("{}-{}", variant.platform(), flavor), + ) + }) + .unwrap_or_default(); + + // The keys used to describe configuration (most specific -> least specific) + vec![ + variant.to_string(), + family_flavor, + variant.family().to_string(), + platform_flavor, + variant.platform().to_string(), + "test".to_string(), + ] +} + +/// All configurations for a specific config level, i.e `-` +#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq, Clone)] +#[serde(deny_unknown_fields)] +pub struct GenericConfig { + #[serde(default)] + aarch64: GenericVariantConfig, + #[serde(default)] + x86_64: GenericVariantConfig, + #[serde(default, flatten)] + config: GenericVariantConfig, + #[serde(default)] + configuration: HashMap, + #[serde(rename = "test-type")] + test_type: Option, +} + +impl GenericConfig { + /// Get the configuration for a specific arch. + pub fn for_arch(&self, arch: S) -> GenericVariantConfig + where + S: Into, + { + match arch.into().as_str() { + "x86_64" => self.x86_64.clone(), + "aarch64" => self.aarch64.clone(), + _ => Default::default(), + } + } + + /// Get the configuration for a specific test type. + pub fn test(&self, test_type: S) -> GenericConfig + where + S: AsRef, + { + self.configuration + .get(test_type.as_ref()) + .cloned() + .unwrap_or_default() + } + + /// Get a set of `GenericConfig`s following test types (test_type -> generic config). + fn test_configs(&self, test_type: S) -> (Vec, String) + where + S: AsRef, + { + // A vec containing all relevant test configs for this `GenericConfig` starting with + // `test_type` and ending with the `GenericConfig` itself. + let mut configs = Vec::new(); + // Track the last test_type that we added to `configs` + let mut cur_test_type = test_type.as_ref().to_string(); + loop { + // Add the config for the current test type (if the config doesn't exist, an empty + // config is added) + let test_config = self.test(&cur_test_type); + configs.push(test_config.clone()); + // If the current test config specifies another test type, that test type needs to be + // added to the configurations. + if let Some(test_type) = test_config.test_type.to_owned() { + trace!("Test-type '{cur_test_type}' resolves to '{test_type}'"); + cur_test_type = test_type; + } else { + break; + } + } + + // Add the `self` config + configs.push(self.clone()); + (configs, cur_test_type) + } +} + +/// The configuration for a specific config level (-). This may or may not be arch +/// specific depending on it's location in `GenericConfig`. +#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq, Clone)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "kebab-case")] +pub struct GenericVariantConfig { + /// The names of all clusters this variant should be tested over. This is particularly useful + /// for testing Bottlerocket on ipv4 and ipv6 clusters. + #[serde(default)] + pub cluster_names: Vec, + /// The instance type that instances should be launched with + pub instance_type: Option, + /// Specify how Bottlerocket instances should be launched (ec2, karpenter) + pub resource_agent_type: Option, + /// Launch instances with the following Block Device Mapping + #[serde(default)] + pub block_device_mapping: Vec, + /// The secrets needed by the agents + #[serde(default)] + pub secrets: BTreeMap, + /// The role that should be assumed for this particular variant + pub agent_role: Option, + /// The location of the sonobuoy testing image + pub sonobuoy_image: Option, + /// The custom images used for conformance testing + pub conformance_image: Option, + /// The custom registry used for conformance testing + pub conformance_registry: Option, + /// The endpoint IP to reserve for the vSphere control plane VMs when creating a K8s cluster + pub control_plane_endpoint: Option, + /// The path to userdata that should be used for Bottlerocket launch + pub userdata: Option, + /// The directory containing Bottlerocket images. For metal, this is the directory containing + /// gzipped images. + pub os_image_dir: Option, + /// The hardware that should be used for provisioning Bottlerocket. For metal, this is the + /// hardware csv that is passed to EKS Anywhere. + pub hardware_csv: Option, + /// The workload tests that should be run + #[serde(default)] + pub workloads: BTreeMap, + #[serde(default)] + pub dev: DeveloperConfig, +} + +impl GenericVariantConfig { + /// Overwrite the unset values of `self` with the set values of `other` + fn merge(self, other: Self) -> Self { + let cluster_names = if self.cluster_names.is_empty() { + other.cluster_names + } else { + self.cluster_names + }; + + let secrets = if self.secrets.is_empty() { + other.secrets + } else { + self.secrets + }; + + let workloads = if self.workloads.is_empty() { + other.workloads + } else { + self.workloads + }; + + let block_device_mapping = if self.block_device_mapping.is_empty() { + other.block_device_mapping + } else { + self.block_device_mapping + }; + + Self { + cluster_names, + instance_type: self.instance_type.or(other.instance_type), + resource_agent_type: self.resource_agent_type.or(other.resource_agent_type), + block_device_mapping, + secrets, + agent_role: self.agent_role.or(other.agent_role), + sonobuoy_image: self.sonobuoy_image.or(other.sonobuoy_image), + conformance_image: self.conformance_image.or(other.conformance_image), + conformance_registry: self.conformance_registry.or(other.conformance_registry), + control_plane_endpoint: self.control_plane_endpoint.or(other.control_plane_endpoint), + userdata: self.userdata.or(other.userdata), + os_image_dir: self.os_image_dir.or(other.os_image_dir), + hardware_csv: self.hardware_csv.or(other.hardware_csv), + workloads, + dev: self.dev.merge(other.dev), + } + } +} + +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "kebab-case")] +pub enum ResourceAgentType { + Karpenter, + Ec2, +} + +impl Default for ResourceAgentType { + fn default() -> Self { + Self::Ec2 + } +} + +derive_fromstr_from_deserialize!(ResourceAgentType); + +/// The configuration for a specific config level (-). This may or may not be arch +/// specific depending on it's location in `GenericConfig`. +/// The configurable fields here add refined control to TestSys objects. +#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq, Clone)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "kebab-case")] +pub struct DeveloperConfig { + /// Control the destruction behavior of cluster CRDs + pub cluster_destruction_policy: Option, + /// Control the destruction behavior of Bottlerocket CRDs + pub bottlerocket_destruction_policy: Option, + /// Keep test pods running on completion + pub keep_tests_running: Option, + /// Use an alternate account for image lookup + pub image_account_id: Option, + /// Overrides the EKS service endpoint for TestSys agents gathering EKS cluster metadata + /// (only for pre-existing EKS clusters, does not apply to new EKS cluster creation) + pub eks_service_endpoint: Option, + /// A manifest containing the EKS Anywhere binary that should be used for cluster provisioning + pub eks_a_release_manifest_url: Option, +} + +impl DeveloperConfig { + /// Overwrite the unset values of `self` with the set values of `other` + fn merge(self, other: Self) -> Self { + Self { + cluster_destruction_policy: self + .cluster_destruction_policy + .or(other.cluster_destruction_policy), + bottlerocket_destruction_policy: self + .bottlerocket_destruction_policy + .or(other.bottlerocket_destruction_policy), + keep_tests_running: self.keep_tests_running.or(other.keep_tests_running), + image_account_id: self.image_account_id.or(other.image_account_id), + eks_service_endpoint: self.eks_service_endpoint.or(other.eks_service_endpoint), + eks_a_release_manifest_url: self + .eks_a_release_manifest_url + .or(other.eks_a_release_manifest_url), + } + } +} + +/// Fill in the templated cluster name with `arch` and `variant`. +pub fn rendered_cluster_name(cluster_name: String, arch: S1, variant: S2) -> Result +where + S1: Into, + S2: Into, +{ + let mut cluster_template = Handlebars::new(); + cluster_template.register_template_string("cluster_name", cluster_name)?; + Ok(cluster_template.render( + "cluster_name", + &btreemap! {"arch".to_string() => arch.into(), "variant".to_string() => variant.into()}, + )?) +} + +#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq, Clone)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "kebab-case")] +pub struct TestsysImages { + pub eks_resource_agent_image: Option, + pub ecs_resource_agent_image: Option, + pub vsphere_k8s_cluster_resource_agent_image: Option, + pub metal_k8s_cluster_resource_agent_image: Option, + pub ec2_resource_agent_image: Option, + pub ec2_karpenter_resource_agent_image: Option, + pub vsphere_vm_resource_agent_image: Option, + pub sonobuoy_test_agent_image: Option, + pub ecs_test_agent_image: Option, + pub migration_test_agent_image: Option, + pub k8s_workload_agent_image: Option, + pub ecs_workload_agent_image: Option, + pub controller_image: Option, + pub testsys_agent_pull_secret: Option, +} + +impl TestsysImages { + /// Create an images config for a specific registry. + pub fn new(registry: S, tag: Option) -> Self + where + S: Into, + { + let registry = registry.into(); + let tag = tag.unwrap_or_else(|| format!("v{TESTSYS_VERSION}")); + Self { + eks_resource_agent_image: Some(format!("{registry}/eks-resource-agent:{tag}")), + ecs_resource_agent_image: Some(format!("{registry}/ecs-resource-agent:{tag}")), + vsphere_k8s_cluster_resource_agent_image: Some(format!( + "{registry}/vsphere-k8s-cluster-resource-agent:{tag}" + )), + metal_k8s_cluster_resource_agent_image: Some(format!( + "{registry}/metal-k8s-cluster-resource-agent:{tag}" + )), + ec2_resource_agent_image: Some(format!("{registry}/ec2-resource-agent:{tag}")), + ec2_karpenter_resource_agent_image: Some(format!( + "{registry}/ec2-karpenter-resource-agent:{tag}" + )), + vsphere_vm_resource_agent_image: Some(format!( + "{registry}/vsphere-vm-resource-agent:{tag}" + )), + sonobuoy_test_agent_image: Some(format!("{registry}/sonobuoy-test-agent:{tag}")), + ecs_test_agent_image: Some(format!("{registry}/ecs-test-agent:{tag}")), + migration_test_agent_image: Some(format!("{registry}/migration-test-agent:{tag}")), + k8s_workload_agent_image: Some(format!("{registry}/k8s-workload-agent:{tag}")), + ecs_workload_agent_image: Some(format!("{registry}/ecs-workload-agent:{tag}")), + controller_image: Some(format!("{registry}/controller:{tag}")), + testsys_agent_pull_secret: None, + } + } + + pub fn merge(self, other: Self) -> Self { + Self { + eks_resource_agent_image: self + .eks_resource_agent_image + .or(other.eks_resource_agent_image), + ecs_resource_agent_image: self + .ecs_resource_agent_image + .or(other.ecs_resource_agent_image), + vsphere_k8s_cluster_resource_agent_image: self + .vsphere_k8s_cluster_resource_agent_image + .or(other.vsphere_k8s_cluster_resource_agent_image), + metal_k8s_cluster_resource_agent_image: self + .metal_k8s_cluster_resource_agent_image + .or(other.metal_k8s_cluster_resource_agent_image), + vsphere_vm_resource_agent_image: self + .vsphere_vm_resource_agent_image + .or(other.vsphere_vm_resource_agent_image), + ec2_resource_agent_image: self + .ec2_resource_agent_image + .or(other.ec2_resource_agent_image), + ec2_karpenter_resource_agent_image: self + .ec2_karpenter_resource_agent_image + .or(other.ec2_karpenter_resource_agent_image), + sonobuoy_test_agent_image: self + .sonobuoy_test_agent_image + .or(other.sonobuoy_test_agent_image), + ecs_test_agent_image: self.ecs_test_agent_image.or(other.ecs_test_agent_image), + migration_test_agent_image: self + .migration_test_agent_image + .or(other.migration_test_agent_image), + k8s_workload_agent_image: self + .k8s_workload_agent_image + .or(other.k8s_workload_agent_image), + ecs_workload_agent_image: self + .ecs_workload_agent_image + .or(other.ecs_workload_agent_image), + controller_image: self.controller_image.or(other.controller_image), + testsys_agent_pull_secret: self + .testsys_agent_pull_secret + .or(other.testsys_agent_pull_secret), + } + } + + pub fn public_images() -> Self { + Self::new("public.ecr.aws/bottlerocket-test-system", None) + } +} + +mod error { + use snafu::Snafu; + use std::io; + use std::path::PathBuf; + + #[derive(Debug, Snafu)] + #[snafu(visibility(pub(super)))] + pub enum Error { + #[snafu(display("Failed to read '{}': {}", path.display(), source))] + File { path: PathBuf, source: io::Error }, + + #[snafu(display("Invalid config file at '{}': {}", path.display(), source))] + InvalidToml { + path: PathBuf, + source: toml::de::Error, + }, + + #[snafu(display("Invalid lock file at '{}': {}", path.display(), source))] + InvalidLock { + path: PathBuf, + source: serde_yaml::Error, + }, + + #[snafu(display("Missing config: {}", what))] + MissingConfig { what: String }, + + #[snafu(display("Failed to get parent of path: {}", path.display()))] + Parent { path: PathBuf }, + + #[snafu( + context(false), + display("Failed to create template for cluster name: {}", source) + )] + TemplateError { + #[snafu(source(from(handlebars::TemplateError, Box::new)))] + source: Box, + }, + + #[snafu( + context(false), + display("Failed to render templated cluster name: {}", source) + )] + RenderError { source: handlebars::RenderError }, + } +} diff --git a/testsys-launcher/testsys/Cargo.toml b/testsys-launcher/testsys/Cargo.toml new file mode 100644 index 00000000..11471a80 --- /dev/null +++ b/testsys-launcher/testsys/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "testsys" +version = "0.1.0" +authors = [ + "Ethan Pullen ", + "Matt Briggs ", +] +license = "Apache-2.0 OR MIT" +edition = "2021" +publish = false + +[dependencies] +async-trait = "0.1" +aws-config = { version = "1", default-features = false, features = ["credentials-process", "default-https-client", "rt-tokio" ] } +aws-sdk-ec2 = { version = "1", default-features = false, features = ["default-https-client", "rt-tokio"] } +base64 = "0.22" +bottlerocket-types = { version = "0.0.17", path = "../../bottlerocket/types" } +bottlerocket-variant = { version = "0.1", path = "../bottlerocket-variant" } +clap = { version = "4", features = ["derive", "env"] } +env_logger = "0.11" +futures = "0.3" +handlebars = "5" +log = "0.4" +maplit = "1" +testsys-model = { version = "0.0.17", path = "../../model" } +pubsys-config = { version = "0.1", path = "../pubsys-config" } +fastrand = "2" +serde = "1" +serde_json = "1" +serde_plain = "1" +serde_yaml = "0.9" +snafu = "0.8" +terminal_size = "0.4" +testsys-config = { version = "0.1", path = "../testsys-config" } +tokio = { version = "1", features = ["fs", "macros", "rt-multi-thread"] } +unescape = "0.1" +url = "2" diff --git a/testsys-launcher/testsys/Test.toml.example b/testsys-launcher/testsys/Test.toml.example new file mode 100644 index 00000000..426850a4 --- /dev/null +++ b/testsys-launcher/testsys/Test.toml.example @@ -0,0 +1,125 @@ +# This is an example testing configuration for TestSys, the tool that is used to validate +# Bottlerocket builds. + +# This section contains configuration details for all testing +[test] + +# The repo from `Infra.toml` that should be used for Bottlerocket update images. It may be useful to +# create a repo in `Infra.toml` that contains the infrastructure needed for testing +repo = "default" + +# The registry containing alternate TestSys agent images +testsys-image-registry = "public.ecr.aws/bottlerocket-test-system" + +# The tag that should be used with `testsys-images-registry` for image pulls +testsys-images-registry = "latest" + +# The URI for the EKS resource agent that should be used. An individual agent's provided URI will be +# used even if `testsys-image-registry` is present. +eks-resource-agent-image = "public.ecr.aws/bottlerocket-test-system/eks_resource_agent:v0.0.2" + +# Test Configurations +# +# Testing requirements tend to differ by variant and architecture. This configuration file provides +# the ability to set values that apply generally to a broad group of similar variants, and to +# override those values at a more granular level. For example, you can set a value for all `aws-k8s` +# variants, then override that value for 'aws-k8s-nvidia' variants, and further override the value +# for 'aws-k8s-nvidia'.aarch64 builds. +# +# The mechanism for resolving configuration values has the following order of precedence: +# +# ''.ARCH +# '' +# '-'.ARCH +# '-' +# ''.ARCH +# '' +# '-'.ARCH +# '-' +# ''.ARCH +# '' +# +# For concrete example, given a variant such as `aws-k8s-1.23-nvidia` with the architecture +# `x86_64`, configurations will have the following order of precedence: +# ['aws-k8s-1.23-nvidia'.x86_64] +# ['aws-k8s-1.23-nvidia'] +# ['aws-k8s-nvidia'.x86_64] +# ['aws-k8s-nvidia'] +# ['aws-k8s'.x86_64] +# ['aws-k8s'] +# ['aws-nvidia'.x86_64] +# ['aws-nvidia'] +# ['aws'.x86_64] +# ['aws'] +# +# Configurable values: +# +# cluster-names: +# All clusters the variant should be tested over. Cluster naming supports templated strings, and +# both `arch` and `variant` are provided as variables (`{{arch}}-{{variant}}`). +# +# instance-type: +# The instance type that should be used for testing. +# +# secrets: +# A map containing the names of all kubernetes secrets needed for resource creation and testing. +# +# agent-role: +# The role that should be assumed by each test and resource agent. +# +# conformance-image: (K8s only) +# Specify a custom image for conformance testing. For `aws-k8s` variants this will be used as a +# custom Kubernetes conformance image for Sonobuoy. +# +# conformance-registry: (K8s only) +# Specify a custom registry for conformance testing images. +# For `aws-k8s` variants this will be used as the Sonobuoy e2e registry. +# +# Note: values passed by command line argument will take precedence over those passed by environment +# variable, and both take precedence over values set by `Test.toml`. + +# Additional fields are configurable with the `dev` table. +# See `DeveloperConfig` for individual fields. + +# Example Configurations + +# Configuration for all variants with the `aws` platform. +[aws] +agent-role = "" + +# Configuration for all nvidia AWS variants on x86_64 (platform-flavor level configuration) +[aws-nvidia.x86_64] +instance-type = "p3.2xlarge" + +# Configuration for all nvidia AWS variants on aarch64 (platform-flavor level configuration) +[aws-nvidia.aarch64] +instance-type = "g5g.2xlarge" + +# Configuration for all `aws-k8s` variants testing (family level configuration). +[aws-k8s] +# A single role can be assumed by agents to test all `aws-k8s` variants in a separate +# testing account. +agent-role = "arn:aws:iam:::role/" + +# The cluster name templating can be defined for all `aws-k8s` variants. To test on ipv4 and ipv6 +# clusters, the following templates could be used. Note: TestSys does not currently support creating +# ipv6 clusters, so the ipv6 cluster must already exist. +cluster-names = ["{{arch}}-{{variant}}", "{{arch}}-{{variant}}-ipv6"] + +# A custom conformance registry may be needed for testing if image pull reliability is a concern. +conformance-registry = ".dkr.ecr.cn-north-1.amazonaws.com.cn" + +# If testing using a kind cluster, AWS credentials need to be passed as a K8s secret. +secrets = {"awsCreds" = "myAwsCredentials"} + +# Configuration for all nvidia AWS variants on x86_64 (family-flavor level configuration) +[aws-ecs-nvidia.x86_64] +instance-type = "p3.2xlarge" + +# Configuration for all nvidia AWS variants on aarch64 (family-flavor level configuration) +[aws-ecs-nvidia.aarch64] +instance-type = "g5g.2xlarge" + +# Configuration for only the `aws-k8s-1.32` variant (variant level configuration). +["aws-k8s-1.32".aarch64] +conformance-image = "" diff --git a/testsys-launcher/testsys/src/aws_ecs.rs b/testsys-launcher/testsys/src/aws_ecs.rs new file mode 100644 index 00000000..53ebf946 --- /dev/null +++ b/testsys-launcher/testsys/src/aws_ecs.rs @@ -0,0 +1,291 @@ +use crate::aws_resources::{ami, ami_name, ec2_crd, get_ami_id}; +use crate::crds::{ + BottlerocketInput, ClusterInput, CrdCreator, CrdInput, CreateCrdOutput, MigrationInput, + TestInput, +}; +use crate::error::{self, Result}; +use crate::migration::migration_crd; +use bottlerocket_types::agent_config::{ + ClusterType, EcsClusterConfig, EcsTestConfig, EcsWorkloadTestConfig, WorkloadTest, +}; +use log::debug; +use maplit::btreemap; +use snafu::{OptionExt, ResultExt}; +use std::collections::BTreeMap; +use testsys_model::{Crd, DestructionPolicy, Test}; + +/// A `CrdCreator` responsible for creating crd related to `aws-ecs` variants. +pub(crate) struct AwsEcsCreator { + pub(crate) region: String, + pub(crate) ami_input: String, + pub(crate) migrate_starting_commit: Option, +} + +#[async_trait::async_trait] +impl CrdCreator for AwsEcsCreator { + /// Determine the AMI from `amis.json`. + async fn image_id(&self, _: &CrdInput) -> Result { + ami(&self.ami_input, &self.region) + } + + /// Determine the starting image from EC2 using standard Bottlerocket naming conventions. + async fn starting_image_id(&self, crd_input: &CrdInput) -> Result { + get_ami_id(ami_name(&crd_input.arch,&crd_input.variant,crd_input.starting_version + .as_ref() + .context(error::InvalidSnafu{ + what: "The starting version must be provided for migration testing" + })?, self.migrate_starting_commit + .as_ref() + .context(error::InvalidSnafu{ + what: "The commit for the starting version must be provided if the starting image id is not" + })?) + , &crd_input.arch, + & self.region, + crd_input.config.dev.image_account_id.as_deref(), + ) + .await + } + + /// Create an ECS cluster CRD with the `cluster_name` in `cluster_input`. + async fn cluster_crd<'a>(&self, cluster_input: ClusterInput<'a>) -> Result { + debug!("Creating ECS cluster CRD"); + // Create labels that will be used for identifying existing CRDs for an ECS cluster. + let labels = cluster_input.crd_input.labels(btreemap! { + "testsys/type".to_string() => "cluster".to_string(), + "testsys/cluster".to_string() => cluster_input.cluster_name.to_string(), + "testsys/region".to_string() => self.region.clone() + }); + + // Check if the cluster already has a CRD in the TestSys cluster. + if let Some(cluster_crd) = cluster_input + .crd_input + .existing_crds( + &labels, + &["testsys/cluster", "testsys/type", "testsys/region"], + ) + .await? + .pop() + { + // Return the name of the existing CRD for the cluster. + debug!("ECS cluster CRD already exists with name '{cluster_crd}'"); + return Ok(CreateCrdOutput::ExistingCrd(cluster_crd)); + } + + // Create the CRD for ECS cluster creation. + let ecs_crd = EcsClusterConfig::builder() + .cluster_name(cluster_input.cluster_name) + .region(Some(self.region.to_owned())) + .assume_role(cluster_input.crd_input.config.agent_role.clone()) + .destruction_policy( + cluster_input + .crd_input + .config + .dev + .cluster_destruction_policy + .to_owned() + .unwrap_or(DestructionPolicy::OnTestSuccess), + ) + .image( + cluster_input + .crd_input + .images + .ecs_resource_agent_image + .as_ref() + .expect("The default ecs resource provider image uri is missing."), + ) + .set_image_pull_secret( + cluster_input + .crd_input + .images + .testsys_agent_pull_secret + .to_owned(), + ) + .set_labels(Some(labels)) + .set_secrets(Some(cluster_input.crd_input.config.secrets.clone())) + .build(cluster_input.cluster_name) + .context(error::BuildSnafu { + what: "ECS cluster CRD", + })?; + + Ok(CreateCrdOutput::NewCrd(Box::new(Crd::Resource(ecs_crd)))) + } + + /// Create an EC2 provider CRD to launch Bottlerocket instances on the cluster created by + /// `cluster_crd`. + async fn bottlerocket_crd<'a>( + &self, + bottlerocket_input: BottlerocketInput<'a>, + ) -> Result { + Ok(CreateCrdOutput::NewCrd(Box::new(Crd::Resource( + ec2_crd(bottlerocket_input, ClusterType::Ecs, &self.region).await?, + )))) + } + + async fn migration_crd<'a>( + &self, + migration_input: MigrationInput<'a>, + ) -> Result { + Ok(CreateCrdOutput::NewCrd(Box::new(Crd::Test(migration_crd( + migration_input, + None, + "ids", + )?)))) + } + + async fn test_crd<'a>(&self, test_input: TestInput<'a>) -> Result { + let cluster_resource_name = test_input + .cluster_crd_name + .as_ref() + .expect("A cluster name is required for migrations"); + let bottlerocket_resource_name = test_input + .bottlerocket_crd_name + .as_ref() + .expect("A cluster name is required for migrations"); + + // Create labels that are used to help filter status. + let labels = test_input.crd_input.labels(btreemap! { + "testsys/type".to_string() => test_input.test_type.to_string(), + "testsys/cluster".to_string() => cluster_resource_name.to_string(), + "testsys/test-name".to_string() => format!( + "{}-{}", + cluster_resource_name, + test_input.name_suffix.unwrap_or(test_input.crd_input.test_flavor.as_str()) + ), + }); + + let test_crd = EcsTestConfig::builder() + .cluster_name_template(cluster_resource_name, "clusterName") + .region(Some(self.region.to_owned())) + .task_count(1) + .assume_role(test_input.crd_input.config.agent_role.to_owned()) + .resources(bottlerocket_resource_name) + .resources(cluster_resource_name) + .set_depends_on(Some(test_input.prev_tests)) + .set_retries(Some(5)) + .image( + test_input + .crd_input + .images + .ecs_test_agent_image + .to_owned() + .expect("The default ECS testing image is missing"), + ) + .set_image_pull_secret( + test_input + .crd_input + .images + .testsys_agent_pull_secret + .to_owned(), + ) + .keep_running( + test_input + .crd_input + .config + .dev + .keep_tests_running + .unwrap_or(false), + ) + .set_secrets(Some(test_input.crd_input.config.secrets.to_owned())) + .set_labels(Some(labels)) + .build(format!( + "{}-{}", + cluster_resource_name, + test_input + .name_suffix + .unwrap_or(test_input.crd_input.test_flavor.as_str()) + )) + .context(error::BuildSnafu { + what: "ECS test CRD", + })?; + + Ok(CreateCrdOutput::NewCrd(Box::new(Crd::Test(test_crd)))) + } + + async fn workload_crd<'a>(&self, test_input: TestInput<'a>) -> Result { + Ok(CreateCrdOutput::NewCrd(Box::new(Crd::Test(workload_crd( + &self.region, + test_input, + )?)))) + } + + fn additional_fields(&self, _test_type: &str) -> BTreeMap { + btreemap! {"region".to_string() => self.region.clone()} + } +} + +/// Create a workload CRD for K8s testing. +pub(crate) fn workload_crd(region: &str, test_input: TestInput) -> Result { + let cluster_resource_name = test_input + .cluster_crd_name + .as_ref() + .expect("A cluster name is required for ECS workload tests"); + let bottlerocket_resource_name = test_input + .bottlerocket_crd_name + .as_ref() + .expect("A bottlerocket resource name is required for ECS workload tests"); + + let labels = test_input.crd_input.labels(btreemap! { + "testsys/type".to_string() => test_input.test_type.to_string(), + "testsys/cluster".to_string() => cluster_resource_name.to_string(), + "testsys/test-name".to_string() => format!( + "{}-{}", + cluster_resource_name, + test_input.name_suffix.unwrap_or("test") + ), + }); + let gpu = test_input.crd_input.variant.variant_flavor() == Some("nvidia"); + let plugins: Vec<_> = test_input + .crd_input + .config + .workloads + .iter() + .map(|(name, image)| WorkloadTest { + name: name.to_string(), + image: image.to_string(), + gpu, + }) + .collect(); + if plugins.is_empty() { + return Err(error::Error::Invalid { + what: "There were no plugins specified in the workload test. + Workloads can be specified in `Test.toml` or via the command line." + .to_string(), + }); + } + + EcsWorkloadTestConfig::builder() + .resources(bottlerocket_resource_name) + .resources(cluster_resource_name) + .set_depends_on(Some(test_input.prev_tests)) + .set_retries(Some(5)) + .image( + test_input + .crd_input + .images + .ecs_workload_agent_image + .to_owned() + .expect("The default K8s workload testing image is missing"), + ) + .set_image_pull_secret( + test_input + .crd_input + .images + .testsys_agent_pull_secret + .to_owned(), + ) + .keep_running(true) + .region(region.to_string()) + .cluster_name_template(cluster_resource_name, "clusterName") + .assume_role(test_input.crd_input.config.agent_role.to_owned()) + .tests(plugins) + .set_secrets(Some(test_input.crd_input.config.secrets.to_owned())) + .set_labels(Some(labels)) + .build(format!( + "{}{}", + cluster_resource_name, + test_input.name_suffix.unwrap_or("-test") + )) + .context(error::BuildSnafu { + what: "Workload CRD", + }) +} diff --git a/testsys-launcher/testsys/src/aws_k8s.rs b/testsys-launcher/testsys/src/aws_k8s.rs new file mode 100644 index 00000000..2a0ee56e --- /dev/null +++ b/testsys-launcher/testsys/src/aws_k8s.rs @@ -0,0 +1,239 @@ +use crate::aws_resources::{ami, ami_name, ec2_crd, ec2_karpenter_crd, get_ami_id}; +use crate::base64; +use crate::crds::{ + BottlerocketInput, ClusterInput, CrdCreator, CrdInput, CreateCrdOutput, MigrationInput, + TestInput, +}; +use crate::error::{self, Result}; +use crate::migration::migration_crd; +use crate::sonobuoy::{sonobuoy_crd, workload_crd}; +use bottlerocket_types::agent_config::{ + ClusterType, CreationPolicy, EksClusterConfig, EksctlConfig, K8sVersion, +}; +use maplit::btreemap; +use serde_yaml::Value; +use snafu::{OptionExt, ResultExt}; +use std::collections::BTreeMap; +use std::str::FromStr; +use testsys_config::ResourceAgentType; +use testsys_model::{Crd, DestructionPolicy}; + +/// A `CrdCreator` responsible for creating crd related to `aws-k8s` variants. +pub(crate) struct AwsK8sCreator { + pub(crate) region: String, + pub(crate) ami_input: String, + pub(crate) migrate_starting_commit: Option, +} + +#[async_trait::async_trait] +impl CrdCreator for AwsK8sCreator { + /// Determine the AMI from `amis.json`. + async fn image_id(&self, _: &CrdInput) -> Result { + ami(&self.ami_input, &self.region) + } + + /// Determine the starting image from EC2 using standard Bottlerocket naming conventions. + async fn starting_image_id(&self, crd_input: &CrdInput) -> Result { + get_ami_id(ami_name(&crd_input.arch,&crd_input.variant,crd_input.starting_version + .as_ref() + .context(error::InvalidSnafu{ + what: "The starting version must be provided for migration testing" + })?, self.migrate_starting_commit + .as_ref() + .context(error::InvalidSnafu{ + what: "The commit for the starting version must be provided if the starting image id is not" + })?) + , &crd_input.arch, + & self.region, + crd_input.config.dev.image_account_id.as_deref(), + ) + .await + } + + /// Create an EKS cluster CRD with the `cluster_name` in `cluster_input`. + async fn cluster_crd<'a>(&self, cluster_input: ClusterInput<'a>) -> Result { + let cluster_version = + K8sVersion::from_str(cluster_input.crd_input.variant.version().context( + error::MissingSnafu { + item: "K8s version".to_string(), + what: "aws-k8s variant".to_string(), + }, + )?) + .map_err(|_| error::Error::K8sVersion { + version: cluster_input.crd_input.variant.to_string(), + })?; + + let (cluster_name, region, config) = match cluster_input.cluster_config { + Some(config) => { + let (cluster_name, region) = cluster_config_data(config)?; + ( + cluster_name, + region, + EksctlConfig::File { + encoded_config: base64::encode(config), + }, + ) + } + None => ( + cluster_input.cluster_name.to_string(), + self.region.clone(), + EksctlConfig::Args { + cluster_name: cluster_input.cluster_name.to_string(), + region: Some(self.region.clone()), + zones: None, + version: Some(cluster_version), + }, + ), + }; + + let labels = cluster_input.crd_input.labels(btreemap! { + "testsys/type".to_string() => "cluster".to_string(), + "testsys/cluster".to_string() => cluster_name.to_string(), + "testsys/region".to_string() => region.clone() + }); + + // Check if the cluster already has a crd + if let Some(cluster_crd) = cluster_input + .crd_input + .existing_crds( + &labels, + &["testsys/cluster", "testsys/type", "testsys/region"], + ) + .await? + .pop() + { + return Ok(CreateCrdOutput::ExistingCrd(cluster_crd)); + } + + let eks_crd = EksClusterConfig::builder() + .creation_policy(CreationPolicy::IfNotExists) + .eks_service_endpoint( + cluster_input + .crd_input + .config + .dev + .eks_service_endpoint + .clone(), + ) + .assume_role(cluster_input.crd_input.config.agent_role.clone()) + .config(config) + .image( + cluster_input + .crd_input + .images + .eks_resource_agent_image + .to_owned() + .expect("Missing default image for EKS resource agent"), + ) + .set_image_pull_secret( + cluster_input + .crd_input + .images + .testsys_agent_pull_secret + .clone(), + ) + .set_labels(Some(labels)) + .set_secrets(Some(cluster_input.crd_input.config.secrets.clone())) + .destruction_policy( + cluster_input + .crd_input + .config + .dev + .cluster_destruction_policy + .to_owned() + .unwrap_or(DestructionPolicy::Never), + ) + .build(cluster_name) + .context(error::BuildSnafu { + what: "EKS cluster CRD", + })?; + + Ok(CreateCrdOutput::NewCrd(Box::new(Crd::Resource(eks_crd)))) + } + + /// Create an EC2 provider CRD to launch Bottlerocket instances on the cluster created by + /// `cluster_crd`. + async fn bottlerocket_crd<'a>( + &self, + bottlerocket_input: BottlerocketInput<'a>, + ) -> Result { + Ok(CreateCrdOutput::NewCrd(Box::new(Crd::Resource( + match bottlerocket_input + .crd_input + .config + .resource_agent_type + .to_owned() + .unwrap_or_default() + { + ResourceAgentType::Ec2 => { + ec2_crd(bottlerocket_input, ClusterType::Eks, &self.region).await? + } + ResourceAgentType::Karpenter => { + ec2_karpenter_crd(bottlerocket_input, &self.region).await? + } + }, + )))) + } + + async fn migration_crd<'a>( + &self, + migration_input: MigrationInput<'a>, + ) -> Result { + Ok(CreateCrdOutput::NewCrd(Box::new(Crd::Test(migration_crd( + migration_input, + None, + "ids", + )?)))) + } + + async fn test_crd<'a>(&self, test_input: TestInput<'a>) -> Result { + Ok(CreateCrdOutput::NewCrd(Box::new(Crd::Test(sonobuoy_crd( + test_input, + )?)))) + } + + async fn workload_crd<'a>(&self, test_input: TestInput<'a>) -> Result { + Ok(CreateCrdOutput::NewCrd(Box::new(Crd::Test(workload_crd( + test_input, + )?)))) + } + + fn additional_fields(&self, _test_type: &str) -> BTreeMap { + btreemap! {"region".to_string() => self.region.clone()} + } +} + +/// Converts a eksctl cluster config to a `serde_yaml::Value` and extracts the cluster name and +/// region from it. +fn cluster_config_data(cluster_config: &str) -> Result<(String, String)> { + let config: Value = serde_yaml::from_str(cluster_config).context(error::SerdeYamlSnafu { + what: "Unable to deserialize cluster config", + })?; + + let (cluster_name, region) = config + .get("metadata") + .map(|metadata| { + ( + metadata.get("name").and_then(|name| name.as_str()), + metadata.get("region").and_then(|region| region.as_str()), + ) + }) + .context(error::MissingSnafu { + item: "metadata", + what: "eksctl config", + })?; + Ok(( + cluster_name + .context(error::MissingSnafu { + item: "name", + what: "eksctl config metadata", + })? + .to_string(), + region + .context(error::MissingSnafu { + item: "region", + what: "eksctl config metadata", + })? + .to_string(), + )) +} diff --git a/testsys-launcher/testsys/src/aws_resources.rs b/testsys-launcher/testsys/src/aws_resources.rs new file mode 100644 index 00000000..ac6f2330 --- /dev/null +++ b/testsys-launcher/testsys/src/aws_resources.rs @@ -0,0 +1,346 @@ +use crate::crds::BottlerocketInput; +use crate::error::{self, Result}; +use aws_sdk_ec2::config::Region; +use aws_sdk_ec2::types::{Filter, Image}; +use bottlerocket_types::agent_config::{ + BlockDeviceMappingConfig, ClusterType, CustomUserData, Ec2Config, Ec2KarpenterConfig, +}; +use maplit::btreemap; +use serde::Deserialize; +use snafu::{ensure, OptionExt, ResultExt}; +use std::collections::HashMap; +use std::fs::File; +use std::iter::repeat_with; +use testsys_model::{DestructionPolicy, Resource}; + +/// Get the AMI for the given `region` from the `ami_input` file. +pub(crate) fn ami(ami_input: &str, region: &str) -> Result { + let file = File::open(ami_input).context(error::IOSnafu { + what: "Unable to open amis.json", + })?; + // Convert the `ami_input` file to a `HashMap` that maps regions to AMI id. + let amis: HashMap = + serde_json::from_reader(file).context(error::SerdeJsonSnafu { + what: format!("Unable to deserialize '{ami_input}'"), + })?; + // Make sure there are some AMIs present in the `ami_input` file. + ensure!( + !amis.is_empty(), + error::InvalidSnafu { + what: format!("{ami_input} is empty") + } + ); + Ok(amis + .get(region) + .context(error::InvalidSnafu { + what: format!("AMI not found for region '{region}'"), + })? + .id + .clone()) +} + +/// Queries EC2 for the given AMI name. If found, returns Ok(Some(id)), if not returns Ok(None). +pub(crate) async fn get_ami_id( + name: S1, + arch: S2, + region: S3, + account: Option<&str>, +) -> Result +where + S1: Into, + S2: Into, + S3: Into, +{ + // Create the `aws_config` that will be used to search EC2 for AMIs. + // TODO: Follow chain of assumed roles for creating config like pubsys uses. + let config = aws_config::defaults(aws_config::BehaviorVersion::v2025_08_07()) + .region(Region::new(region.into())) + .load() + .await; + let ec2_client = aws_sdk_ec2::Client::new(&config); + // Find all images named `name` on `arch` in the `region`. + let describe_images = ec2_client + .describe_images() + .owners(account.unwrap_or("self")) + .filters(Filter::builder().name("name").values(name).build()) + .filters( + Filter::builder() + .name("image-type") + .values("machine") + .build(), + ) + .filters(Filter::builder().name("architecture").values(arch).build()) + .filters( + Filter::builder() + .name("virtualization-type") + .values("hvm") + .build(), + ) + .send() + .await? + .images; + let images: Vec<&Image> = describe_images.iter().flatten().collect(); + // Make sure there is exactly 1 image that matches the parameters. + if images.len() > 1 { + return Err(error::Error::Invalid { + what: "Unable to determine AMI. Multiple images were found".to_string(), + }); + }; + if let Some(image) = images.last().as_ref() { + Ok(image + .image_id() + .context(error::InvalidSnafu { + what: "No image id for AMI", + })? + .to_string()) + } else { + Err(error::Error::Invalid { + what: "Unable to determine AMI. No images were found".to_string(), + }) + } +} + +/// Get the standard Bottlerocket AMI name. +pub(crate) fn ami_name(arch: &str, variant: &str, version: &str, commit_id: &str) -> String { + format!("bottlerocket-{variant}-{arch}-{version}-{commit_id}") +} + +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct AmiImage { + pub(crate) id: String, +} + +/// Create a CRD to launch Bottlerocket instances on an EKS or ECS cluster. +pub(crate) async fn ec2_crd( + bottlerocket_input: BottlerocketInput<'_>, + cluster_type: ClusterType, + region: &str, +) -> Result { + let cluster_name = bottlerocket_input + .cluster_crd_name + .as_ref() + .expect("A cluster provider is required"); + + // Create the labels for this EC2 provider. + let labels = bottlerocket_input.crd_input.labels(btreemap! { + "testsys/type".to_string() => "instances".to_string(), + "testsys/cluster".to_string() => cluster_name.to_string(), + "testsys/region".to_string() => region.to_string() + }); + + // Find all resources using the same cluster. + let conflicting_resources = bottlerocket_input + .crd_input + .existing_crds( + &labels, + &["testsys/cluster", "testsys/type", "testsys/region"], + ) + .await?; + + let mut ec2_builder = Ec2Config::builder(); + ec2_builder + .node_ami(bottlerocket_input.image_id) + .instance_count(2) + .instance_types::>( + bottlerocket_input + .crd_input + .config + .instance_type + .iter() + .cloned() + .collect(), + ) + .custom_user_data( + bottlerocket_input + .crd_input + .encoded_userdata()? + .map(|encoded_userdata| CustomUserData::Merge { encoded_userdata }), + ) + .cluster_name_template(cluster_name, "clusterName") + .region_template(cluster_name, "region") + .instance_profile_arn_template(cluster_name, "iamInstanceProfileArn") + .assume_role(bottlerocket_input.crd_input.config.agent_role.clone()) + .cluster_type(cluster_type.clone()) + .depends_on(cluster_name) + .image( + bottlerocket_input + .crd_input + .images + .ec2_resource_agent_image + .as_ref() + .expect("Missing default image for EC2 resource agent"), + ) + .set_image_pull_secret( + bottlerocket_input + .crd_input + .images + .testsys_agent_pull_secret + .clone(), + ) + .device_mappings({ + let mappings = bottlerocket_input + .crd_input + .config + .block_device_mapping + .clone(); + if mappings.is_empty() { + None + } else { + Some(mappings) + } + }) + .set_labels(Some(labels)) + .set_conflicts_with(conflicting_resources.into()) + .set_secrets(Some(bottlerocket_input.crd_input.config.secrets.clone())) + .destruction_policy( + bottlerocket_input + .crd_input + .config + .dev + .bottlerocket_destruction_policy + .to_owned() + .unwrap_or(DestructionPolicy::OnTestSuccess), + ); + + // Add in the EKS specific configuration. + if cluster_type == ClusterType::Eks { + ec2_builder + .subnet_ids_template(cluster_name, "publicSubnetIds") + .endpoint_template(cluster_name, "endpoint") + .certificate_template(cluster_name, "certificate") + .cluster_dns_ip_template(cluster_name, "clusterDnsIp") + .security_groups_template(cluster_name, "securityGroups"); + } else { + // The default VPC doesn't attach private subnets to an ECS cluster, so public subnet ids + // are used instead. + ec2_builder + .subnet_ids_template(cluster_name, "publicSubnetIds") + // TODO If this is not set, the crd cannot be serialized since it is a `Vec` not + // `Option`. + .security_groups(Vec::new()); + } + + let suffix: String = repeat_with(fastrand::lowercase).take(4).collect(); + ec2_builder + .build(format!("{cluster_name}-instances-{suffix}")) + .context(error::BuildSnafu { + what: "EC2 instance provider CRD", + }) +} + +/// Create a CRD to launch Bottlerocket instances on an EKS or ECS cluster. +pub(crate) async fn ec2_karpenter_crd( + bottlerocket_input: BottlerocketInput<'_>, + region: &str, +) -> Result { + let cluster_name = bottlerocket_input + .cluster_crd_name + .as_ref() + .expect("A cluster provider is required"); + + // Create the labels for this EC2 provider. + let labels = bottlerocket_input.crd_input.labels(btreemap! { + "testsys/type".to_string() => "instances".to_string(), + "testsys/cluster".to_string() => cluster_name.to_string(), + "testsys/region".to_string() => region.to_string() + }); + + // Find all resources using the same cluster. + let conflicting_resources = bottlerocket_input + .crd_input + .existing_crds( + &labels, + &["testsys/cluster", "testsys/type", "testsys/region"], + ) + .await?; + + // If no mappings were provided use a standard mapping as a default + let device_mappings = if bottlerocket_input + .crd_input + .config + .block_device_mapping + .is_empty() + { + vec![ + BlockDeviceMappingConfig { + name: "/dev/xvda".to_string(), + volume_type: "gp3".to_string(), + volume_size: 4, + delete_on_termination: true, + }, + BlockDeviceMappingConfig { + name: "/dev/xvdb".to_string(), + volume_type: "gp3".to_string(), + volume_size: 20, + delete_on_termination: true, + }, + ] + } else { + bottlerocket_input + .crd_input + .config + .block_device_mapping + .clone() + }; + + let mut ec2_builder = Ec2KarpenterConfig::builder(); + ec2_builder + .node_ami(bottlerocket_input.image_id) + .instance_types::>( + bottlerocket_input + .crd_input + .config + .instance_type + .iter() + .cloned() + .collect(), + ) + .custom_user_data( + bottlerocket_input + .crd_input + .encoded_userdata()? + .map(|encoded_userdata| CustomUserData::Merge { encoded_userdata }), + ) + .cluster_name_template(cluster_name, "clusterName") + .region_template(cluster_name, "region") + .subnet_ids_template(cluster_name, "publicSubnetIds") + .endpoint_template(cluster_name, "endpoint") + .cluster_sg_template(cluster_name, "clusterSg") + .device_mappings(device_mappings) + .assume_role(bottlerocket_input.crd_input.config.agent_role.clone()) + .depends_on(cluster_name) + .image( + bottlerocket_input + .crd_input + .images + .ec2_karpenter_resource_agent_image + .as_ref() + .expect("Missing default image for EC2 resource agent"), + ) + .set_image_pull_secret( + bottlerocket_input + .crd_input + .images + .testsys_agent_pull_secret + .clone(), + ) + .set_labels(Some(labels)) + .set_conflicts_with(conflicting_resources.into()) + .set_secrets(Some(bottlerocket_input.crd_input.config.secrets.clone())) + .destruction_policy( + bottlerocket_input + .crd_input + .config + .dev + .bottlerocket_destruction_policy + .to_owned() + .unwrap_or(DestructionPolicy::OnTestSuccess), + ); + + let suffix: String = repeat_with(fastrand::lowercase).take(4).collect(); + ec2_builder + .build(format!("{cluster_name}-karpenter-{suffix}")) + .context(error::BuildSnafu { + what: "EC2 instance provider CRD", + }) +} diff --git a/testsys-launcher/testsys/src/base64.rs b/testsys-launcher/testsys/src/base64.rs new file mode 100644 index 00000000..0c96f282 --- /dev/null +++ b/testsys-launcher/testsys/src/base64.rs @@ -0,0 +1,9 @@ +use base64::alphabet::STANDARD; +use base64::engine::{GeneralPurpose, GeneralPurposeConfig}; +use base64::Engine; + +/// This function became deprecated in the base64 library but its interface is much simpler than +/// what replaced it. Rather than change all of our call sites we retain the simple interface here. +pub(crate) fn encode>(input: T) -> String { + GeneralPurpose::new(&STANDARD, GeneralPurposeConfig::default()).encode(input) +} diff --git a/testsys-launcher/testsys/src/crds.rs b/testsys-launcher/testsys/src/crds.rs new file mode 100644 index 00000000..cb3dddf5 --- /dev/null +++ b/testsys-launcher/testsys/src/crds.rs @@ -0,0 +1,787 @@ +use crate::base64; +use crate::error::{self, Result}; +use crate::run::{KnownTestType, TestType}; +use bottlerocket_types::agent_config::TufRepoConfig; +use bottlerocket_variant::Variant; +use handlebars::Handlebars; +use log::{debug, info, warn}; +use maplit::btreemap; +use pubsys_config::RepoConfig; +use serde::Deserialize; +use snafu::{OptionExt, ResultExt}; +use std::collections::BTreeMap; +use std::fs; +use std::path::PathBuf; +use testsys_config::{rendered_cluster_name, GenericVariantConfig, TestsysImages}; +use testsys_model::constants::{API_VERSION, NAMESPACE}; +use testsys_model::test_manager::{SelectionParams, TestManager}; +use testsys_model::Crd; + +/// A type that is used for the creation of all CRDs. +pub struct CrdInput<'a> { + pub client: &'a TestManager, + pub arch: String, + pub variant: Variant, + pub config: GenericVariantConfig, + pub repo_config: RepoConfig, + pub test_flavor: String, + pub starting_version: Option, + pub migrate_to_version: Option, + pub build_id: Option, + /// `CrdCreator::starting_image_id` function should be used instead of using this field, so + /// it is not externally visible. + pub(crate) starting_image_id: Option, + pub(crate) test_type: TestType, + pub(crate) tests_directory: PathBuf, + pub images: TestsysImages, +} + +impl CrdInput<'_> { + /// Retrieve the TUF repo information from `Infra.toml` + pub fn tuf_repo_config(&self) -> Option { + if let (Some(metadata_base_url), Some(targets_url)) = ( + &self.repo_config.metadata_base_url, + &self.repo_config.targets_url, + ) { + debug!( + "Using TUF metadata from Infra.toml, metadata: '{metadata_base_url}', targets: '{targets_url}'" + ); + Some(TufRepoConfig { + metadata_url: format!("{}{}/{}/", metadata_base_url, &self.variant, &self.arch), + targets_url: targets_url.to_string(), + }) + } else { + warn!("No TUF metadata was found in Infra.toml using the default TUF repos"); + None + } + } + + /// Create a set of labels for the CRD by adding `additional_labels` to the standard labels. + pub fn labels(&self, additional_labels: BTreeMap) -> BTreeMap { + let mut labels = btreemap! { + "testsys/arch".to_string() => self.arch.to_string(), + "testsys/variant".to_string() => self.variant.to_string(), + "testsys/build-id".to_string() => self.build_id.to_owned().unwrap_or_default(), + "testsys/test-type".to_string() => self.test_type.to_string(), + }; + let mut add_labels = additional_labels; + labels.append(&mut add_labels); + labels + } + + /// Determine all CRDs that have the same value for each `id_labels` as `labels`. + pub async fn existing_crds( + &self, + labels: &BTreeMap, + id_labels: &[&str], + ) -> Result> { + // Create a single string containing all `label=value` pairs. + let checks = id_labels + .iter() + .map(|label| { + labels + .get(&label.to_string()) + .map(|value| format!("{label}={value}")) + .context(error::InvalidSnafu { + what: format!("The label '{label}' was missing"), + }) + }) + .collect::>>()? + .join(","); + + // Create a list of all CRD names that match all of the specified labels. + Ok(self + .client + .list(&SelectionParams { + labels: Some(checks), + ..Default::default() + }) + .await? + .iter() + .filter_map(Crd::name) + .collect()) + } + + /// Use the provided userdata path to create the encoded userdata. + pub fn encoded_userdata(&self) -> Result> { + let userdata_path = match self.config.userdata.as_ref() { + Some(userdata) => self.custom_userdata_file_path(userdata)?, + None => return Ok(None), + }; + + info!("Using userdata at '{}'", userdata_path.display()); + + let userdata = std::fs::read_to_string(&userdata_path).context(error::FileSnafu { + path: userdata_path, + })?; + + Ok(Some(base64::encode(userdata))) + } + + /// Find the userdata file for the test type + fn custom_userdata_file_path(&self, userdata: &str) -> Result { + let test_type = &self.test_type.to_string(); + + // List all acceptable paths to the custom crd to allow users some freedom in the way + // `tests` is organized. + let acceptable_paths = vec![ + // Check the absolute path + userdata.into(), + // Check for // + self.tests_directory.join(test_type).join(userdata), + // Check for //.toml + self.tests_directory + .join(test_type) + .join(userdata) + .with_extension("toml"), + // Check for /shared/ + self.tests_directory.join("shared").join(userdata), + // Check for /shared/.toml + self.tests_directory + .join("shared") + .join(userdata) + .with_extension("toml"), + // Check for /shared/userdata/ + self.tests_directory + .join("shared") + .join("userdata") + .join(userdata), + // Check for /shared/userdata/.toml + self.tests_directory + .join("shared") + .join("userdata") + .join(userdata) + .with_extension("toml"), + // Check for the path in the top level directory + PathBuf::new().join(userdata), + ]; + + // Find the first acceptable path that exists and return that. + acceptable_paths + .into_iter() + .find(|path| path.exists()) + .context(error::InvalidSnafu { + what: format!("Could not find userdata '{userdata}' for test type '{test_type}'"), + }) + } + + /// Fill in the templated cluster name with `arch` and `variant`. + fn rendered_cluster_name(&self, raw_cluster_name: String) -> Result { + Ok(rendered_cluster_name( + raw_cluster_name, + self.kube_arch(), + self.kube_variant(), + )?) + } + + /// Get the k8s safe architecture name + fn kube_arch(&self) -> String { + self.arch.replace('_', "-") + } + + /// Get the k8s safe variant name + fn kube_variant(&self) -> String { + self.variant.to_string().replace('.', "") + } + + /// Bottlerocket cluster naming convention. + fn default_cluster_name(&self) -> String { + format!("{}-{}", self.kube_arch(), self.kube_variant()) + } + + /// Get a list of cluster_names for this variant. If there are no cluster names, the default + /// cluster name will be used. + fn cluster_names(&self) -> Result> { + Ok(if self.config.cluster_names.is_empty() { + vec![self.default_cluster_name()] + } else { + self.config + .cluster_names + .iter() + .map(String::to_string) + // Fill the template fields in the clusters name before using it. + .map(|cluster_name| self.rendered_cluster_name(cluster_name)) + .collect::>>()? + }) + } + + /// Creates a `BTreeMap` of all configurable fields from this input + fn config_fields(&self, cluster_name: &str) -> BTreeMap { + btreemap! { + "arch".to_string() => self.arch.clone(), + "variant".to_string() => self.variant.to_string(), + "kube-arch".to_string() => self.kube_arch(), + "kube-variant".to_string() => self.kube_variant(), + "flavor".to_string() => some_or_null(&self.variant.variant_flavor().map(str::to_string)), + "version".to_string() => some_or_null(&self.variant.version().map(str::to_string)), + "cluster-name".to_string() => cluster_name.to_string(), + "instance-type".to_string() => some_or_null(&self.config.instance_type), + "agent-role".to_string() => some_or_null(&self.config.agent_role), + "conformance-image".to_string() => some_or_null(&self.config.conformance_image), + "conformance-registry".to_string() => some_or_null(&self.config.conformance_registry), + "control-plane-endpoint".to_string() => some_or_null(&self.config.control_plane_endpoint), + } + } + + /// Find the crd template file for the given test type + fn custom_crd_template_file_path(&self) -> Option { + let test_type = &self.test_type.to_string(); + // List all acceptable paths to the custom crd to allow users some freedom in the way + // `tests` is organized. + let acceptable_paths = vec![ + // Check for .yaml in the top level directory + PathBuf::new().join(test_type).with_extension("yaml"), + // Check for //.yaml + self.tests_directory + .join(test_type) + .join(test_type) + .with_extension("yaml"), + // Check for //crd.yaml + self.tests_directory.join(test_type).join("crd.yaml"), + // Check for /shared/.yaml + self.tests_directory + .join("shared") + .join(test_type) + .with_extension("yaml"), + // Check for /shared/tests/.yaml + self.tests_directory + .join("shared") + .join("tests") + .join(test_type) + .with_extension("yaml"), + ]; + + // Find the first acceptable path that exists and return that. + acceptable_paths.into_iter().find(|path| path.exists()) + } + + /// Find the cluster config file for the given cluster name and test type. + fn cluster_config_file_path(&self, cluster_name: &str) -> Option { + let test_type = &self.test_type.to_string(); + // List all acceptable paths to the custom crd to allow users some freedom in the way + // `tests` is organized. + let acceptable_paths = vec![ + // Check for //.yaml + self.tests_directory + .join(test_type) + .join(cluster_name) + .with_extension("yaml"), + // Check for /shared/.yaml + self.tests_directory + .join("shared") + .join(cluster_name) + .with_extension("yaml"), + // Check for /shared/cluster-config/.yaml + self.tests_directory + .join("shared") + .join("cluster-config") + .join(cluster_name) + .with_extension("yaml"), + // Check for /shared/clusters/.yaml + self.tests_directory + .join("shared") + .join("clusters") + .join(cluster_name) + .with_extension("yaml"), + // Check for /shared/clusters//cluster.yaml + self.tests_directory + .join("shared") + .join("clusters") + .join(cluster_name) + .join("cluster") + .with_extension("yaml"), + ]; + + // Find the first acceptable path that exists and return that. + acceptable_paths.into_iter().find(|path| path.exists()) + } + + /// Find the resolved cluster config file for the given cluster name and test type if it exists. + fn resolved_cluster_config( + &self, + cluster_name: &str, + additional_fields: &mut BTreeMap, + ) -> Result> { + let path = match self.cluster_config_file_path(cluster_name) { + None => return Ok(None), + Some(path) => path, + }; + info!("Using cluster config at {}", path.display()); + let config = fs::read_to_string(&path).context(error::FileSnafu { path })?; + + let mut fields = self.config_fields(cluster_name); + fields.insert("api-version".to_string(), API_VERSION.to_string()); + fields.insert("namespace".to_string(), NAMESPACE.to_string()); + fields.append(additional_fields); + + let mut handlebars = Handlebars::new(); + handlebars.set_strict_mode(true); + let rendered_config = handlebars.render_template(&config, &fields)?; + + Ok(Some(rendered_config)) + } + + /// Find the hardware csv file for the given hardware csv name and test type. + fn hardware_csv_file_path(&self, hardware_csv: &str) -> Option { + let test_type = &self.test_type.to_string(); + // List all acceptable paths to the custom crd to allow users some freedom in the way + // `tests` is organized. + let acceptable_paths = vec![ + // Check for //.csv + self.tests_directory + .join(test_type) + .join(hardware_csv) + .with_extension("csv"), + // Check for /shared/.csv + self.tests_directory + .join("shared") + .join(hardware_csv) + .with_extension("csv"), + // Check for /shared/cluster-config/.csv + self.tests_directory + .join("shared") + .join("cluster-config") + .join(hardware_csv) + .with_extension("csv"), + // Check for /shared/clusters/.csv + self.tests_directory + .join("shared") + .join("clusters") + .join(hardware_csv) + .with_extension("csv"), + ]; + + // Find the first acceptable path that exists and return that. + acceptable_paths.into_iter().find(|path| path.exists()) + } + + /// Find the resolved cluster config file for the given cluster name and test type if it exists. + fn resolved_hardware_csv(&self) -> Result> { + let hardware_csv = match &self.config.hardware_csv { + Some(hardware_csv) => hardware_csv, + None => return Ok(None), + }; + + // If the hardware csv is csv like, it probably is a csv; otherwise, it is a path to the + // hardware csv. + if hardware_csv.contains(',') { + return Ok(Some(hardware_csv.to_string())); + } + + let path = match self.hardware_csv_file_path(hardware_csv) { + None => return Ok(None), + Some(path) => path, + }; + + info!("Using hardware csv at {}", path.display()); + + let config = fs::read_to_string(&path).context(error::FileSnafu { path })?; + Ok(Some(config)) + } + + fn hardware_for_cluster(&self, cluster_name: &str) -> Result> { + // Check for /shared/clusters//hardware.csv + let path = self + .tests_directory + .join("shared") + .join("clusters") + .join(cluster_name) + .join("hardware") + .with_extension("csv"); + + if !path.exists() { + return Ok(None); + } + + info!("Using hardware csv at {}", path.display()); + + let config = fs::read_to_string(&path).context(error::FileSnafu { path })?; + Ok(Some(config)) + } +} + +/// Take the value of the `Option` or `"null"` if the `Option` was `None` +fn some_or_null(field: &Option) -> String { + field.to_owned().unwrap_or_else(|| "null".to_string()) +} + +/// The `CrdCreator` trait is used to create CRDs. Each variant family should have a `CrdCreator` +/// that is responsible for creating the CRDs needed for testing. +#[async_trait::async_trait] +pub(crate) trait CrdCreator: Sync { + /// Return the image id that should be used for normal testing. + async fn image_id(&self, crd_input: &CrdInput) -> Result; + + /// Return the image id that should be used as the starting point for migration testing. + async fn starting_image_id(&self, crd_input: &CrdInput) -> Result; + + /// Create a CRD for the cluster needed to launch Bottlerocket. If no cluster CRD is + /// needed, `CreateCrdOutput::None` can be returned. + async fn cluster_crd<'a>(&self, cluster_input: ClusterInput<'a>) -> Result; + + /// Create a CRD to launch Bottlerocket. `CreateCrdOutput::None` can be returned if this CRD is + /// not needed. + async fn bottlerocket_crd<'a>( + &self, + bottlerocket_input: BottlerocketInput<'a>, + ) -> Result; + + /// Create a CRD that migrates Bottlerocket from one version to another. + async fn migration_crd<'a>( + &self, + migration_input: MigrationInput<'a>, + ) -> Result; + + /// Create a testing CRD for this variant of Bottlerocket. + async fn test_crd<'a>(&self, test_input: TestInput<'a>) -> Result; + + /// Create a workload testing CRD for this variant of Bottlerocket. + async fn workload_crd<'a>(&self, test_input: TestInput<'a>) -> Result; + + /// Create a set of additional fields that may be used by an externally defined agent on top of + /// the ones in `CrdInput` + fn additional_fields(&self, _test_type: &str) -> BTreeMap { + Default::default() + } + + /// Creates a set of CRDs for the specified variant and test type that can be added to a TestSys + /// cluster. + async fn create_crds( + &self, + test_type: &KnownTestType, + crd_input: &CrdInput, + ) -> Result> { + let mut crds = Vec::new(); + let image_id = match &test_type { + KnownTestType::Migration => { + if let Some(image_id) = &crd_input.starting_image_id { + debug!( + "Using the provided starting image id for migration testing '{image_id}'" + ); + image_id.to_string() + } else { + let image_id = self.starting_image_id(crd_input).await?; + debug!( + "A starting image id was not provided, '{image_id}' will be used instead." + ); + image_id + } + } + _ => self.image_id(crd_input).await?, + }; + for cluster_name in &crd_input.cluster_names()? { + let cluster_output = self + .cluster_crd(ClusterInput { + cluster_name, + image_id: &image_id, + crd_input, + cluster_config: &crd_input.resolved_cluster_config( + cluster_name, + &mut self + .additional_fields(&test_type.to_string()) + .into_iter() + // Add the image id, original test type, and resolved test type in case + // it is needed for cluster creation + .chain(Some(("image-id".to_string(), image_id.clone()))) + .chain(Some(( + "test-flavor".to_string(), + crd_input.test_flavor.clone(), + ))) + .chain(Some(("test-type".to_string(), test_type.to_string()))) + .collect::>(), + )?, + hardware_csv: &crd_input + .resolved_hardware_csv() + .transpose() + .or_else(|| crd_input.hardware_for_cluster(cluster_name).transpose()) + .transpose()?, + }) + .await?; + let cluster_crd_name = cluster_output.crd_name(); + if let Some(crd) = cluster_output.crd() { + debug!("Cluster crd was created for '{cluster_name}'"); + crds.push(crd) + } + let bottlerocket_output = self + .bottlerocket_crd(BottlerocketInput { + cluster_crd_name: &cluster_crd_name, + image_id: image_id.clone(), + _test_type: test_type, + crd_input, + }) + .await?; + let bottlerocket_crd_name = bottlerocket_output.crd_name(); + match &test_type { + KnownTestType::Conformance | KnownTestType::Quick => { + if let Some(crd) = bottlerocket_output.crd() { + debug!("Bottlerocket crd was created for '{cluster_name}'"); + crds.push(crd) + } + let test_output = self + .test_crd(TestInput { + cluster_crd_name: &cluster_crd_name, + bottlerocket_crd_name: &bottlerocket_crd_name, + test_type, + crd_input, + prev_tests: Default::default(), + name_suffix: None, + }) + .await?; + if let Some(crd) = test_output.crd() { + crds.push(crd) + } + } + KnownTestType::Workload => { + if let Some(crd) = bottlerocket_output.crd() { + debug!("Bottlerocket crd was created for '{cluster_name}'"); + crds.push(crd) + } + let test_output = self + .workload_crd(TestInput { + cluster_crd_name: &cluster_crd_name, + bottlerocket_crd_name: &bottlerocket_crd_name, + test_type, + crd_input, + prev_tests: Default::default(), + name_suffix: None, + }) + .await?; + if let Some(crd) = test_output.crd() { + crds.push(crd) + } + } + KnownTestType::Migration => { + if let Some(crd) = bottlerocket_output.crd() { + debug!("Bottlerocket crd was created for '{cluster_name}'"); + crds.push(crd) + } + let mut tests = Vec::new(); + let test_output = self + .test_crd(TestInput { + cluster_crd_name: &cluster_crd_name, + bottlerocket_crd_name: &bottlerocket_crd_name, + test_type, + crd_input, + prev_tests: tests.clone(), + name_suffix: "1-initial".into(), + }) + .await?; + if let Some(name) = test_output.crd_name() { + tests.push(name) + } + if let Some(crd) = test_output.crd() { + crds.push(crd) + } + let migration_output = self + .migration_crd(MigrationInput { + cluster_crd_name: &cluster_crd_name, + bottlerocket_crd_name: &bottlerocket_crd_name, + crd_input, + prev_tests: tests.clone(), + name_suffix: "2-migrate".into(), + migration_direction: MigrationDirection::Upgrade, + }) + .await?; + if let Some(name) = migration_output.crd_name() { + tests.push(name) + } + if let Some(crd) = migration_output.crd() { + crds.push(crd) + } + let test_output = self + .test_crd(TestInput { + cluster_crd_name: &cluster_crd_name, + bottlerocket_crd_name: &bottlerocket_crd_name, + test_type, + crd_input, + prev_tests: tests.clone(), + name_suffix: "3-migrated".into(), + }) + .await?; + if let Some(name) = test_output.crd_name() { + tests.push(name) + } + if let Some(crd) = test_output.crd() { + crds.push(crd) + } + let migration_output = self + .migration_crd(MigrationInput { + cluster_crd_name: &cluster_crd_name, + bottlerocket_crd_name: &bottlerocket_crd_name, + crd_input, + prev_tests: tests.clone(), + name_suffix: "4-migrate".into(), + migration_direction: MigrationDirection::Downgrade, + }) + .await?; + if let Some(name) = migration_output.crd_name() { + tests.push(name) + } + if let Some(crd) = migration_output.crd() { + crds.push(crd) + } + let test_output = self + .test_crd(TestInput { + cluster_crd_name: &cluster_crd_name, + bottlerocket_crd_name: &bottlerocket_crd_name, + test_type, + crd_input, + prev_tests: tests, + name_suffix: "5-final".into(), + }) + .await?; + if let Some(crd) = test_output.crd() { + crds.push(crd) + } + } + } + } + + Ok(crds) + } + + /// Creates a set of CRDs for the specified variant and test type that can be added to a TestSys + /// cluster. + async fn create_custom_crds( + &self, + test_type: &str, + crd_input: &CrdInput, + override_crd_template: Option, + ) -> Result> { + debug!("Creating custom CRDs for '{}' test", test_type); + let crd_template_file_path = &override_crd_template + .or_else(|| crd_input.custom_crd_template_file_path()) + .context(error::InvalidSnafu { + what: format!("A custom yaml file could not be found for test type '{test_type}'"), + })?; + info!( + "Creating custom crd from '{}'", + crd_template_file_path.display() + ); + let mut crds = Vec::new(); + for cluster_name in &crd_input.cluster_names()? { + let mut fields = crd_input.config_fields(cluster_name); + fields.insert("api-version".to_string(), API_VERSION.to_string()); + fields.insert("namespace".to_string(), NAMESPACE.to_string()); + fields.insert("image-id".to_string(), self.image_id(crd_input).await?); + fields.append(&mut self.additional_fields(test_type)); + + let mut handlebars = Handlebars::new(); + handlebars.set_strict_mode(true); + let rendered_manifest = handlebars.render_template( + &std::fs::read_to_string(crd_template_file_path).context(error::FileSnafu { + path: crd_template_file_path, + })?, + &fields, + )?; + + for crd_doc in serde_yaml::Deserializer::from_str(&rendered_manifest) { + let value = + serde_yaml::Value::deserialize(crd_doc).context(error::SerdeYamlSnafu { + what: "Unable to deserialize rendered manifest", + })?; + let mut crd: Crd = + serde_yaml::from_value(value).context(error::SerdeYamlSnafu { + what: "The manifest did not match a `CRD`", + })?; + // Add in the secrets from the config manually. + match &mut crd { + Crd::Test(test) => { + test.spec.agent.secrets = Some(crd_input.config.secrets.clone()) + } + Crd::Resource(resource) => { + resource.spec.agent.secrets = Some(crd_input.config.secrets.clone()) + } + } + crds.push(crd); + } + } + Ok(crds) + } +} + +/// The input used for cluster crd creation +pub struct ClusterInput<'a> { + pub cluster_name: &'a String, + #[allow(dead_code)] + pub image_id: &'a String, + pub crd_input: &'a CrdInput<'a>, + pub cluster_config: &'a Option, + #[allow(dead_code)] + pub hardware_csv: &'a Option, +} + +/// The input used for bottlerocket crd creation +pub struct BottlerocketInput<'a> { + pub cluster_crd_name: &'a Option, + /// The image id that should be used by this CRD + pub image_id: String, + pub _test_type: &'a KnownTestType, + pub crd_input: &'a CrdInput<'a>, +} + +/// The input used for test crd creation +pub struct TestInput<'a> { + pub cluster_crd_name: &'a Option, + pub bottlerocket_crd_name: &'a Option, + pub test_type: &'a KnownTestType, + pub crd_input: &'a CrdInput<'a>, + /// The set of tests that have already been created that are related to this test + pub prev_tests: Vec, + /// The suffix that should be appended to the end of the test name to prevent naming conflicts + pub name_suffix: Option<&'a str>, +} + +/// The input used for migration crd creation +pub struct MigrationInput<'a> { + pub cluster_crd_name: &'a Option, + pub bottlerocket_crd_name: &'a Option, + pub crd_input: &'a CrdInput<'a>, + /// The set of tests that have already been created that are related to this test + pub prev_tests: Vec, + /// The suffix that should be appended to the end of the test name to prevent naming conflicts + pub name_suffix: Option<&'a str>, + pub migration_direction: MigrationDirection, +} + +pub enum MigrationDirection { + Upgrade, + Downgrade, +} + +pub enum CreateCrdOutput { + /// A new CRD was created and needs to be applied to the cluster. + NewCrd(Box), + /// An existing CRD is already representing this object. + ExistingCrd(String), + /// There is no CRD to create for this step of this family. + None, +} + +impl Default for CreateCrdOutput { + fn default() -> Self { + Self::None + } +} + +impl CreateCrdOutput { + /// Get the name of the CRD that was created or already existed + pub(crate) fn crd_name(&self) -> Option { + match self { + CreateCrdOutput::NewCrd(crd) => { + Some(crd.name().expect("A CRD is missing the name field.")) + } + CreateCrdOutput::ExistingCrd(name) => Some(name.to_string()), + CreateCrdOutput::None => None, + } + } + + /// Get the CRD if it was created + pub(crate) fn crd(self) -> Option { + match self { + CreateCrdOutput::NewCrd(crd) => Some(*crd), + CreateCrdOutput::ExistingCrd(_) => None, + CreateCrdOutput::None => None, + } + } +} diff --git a/testsys-launcher/testsys/src/delete.rs b/testsys-launcher/testsys/src/delete.rs new file mode 100644 index 00000000..c671b444 --- /dev/null +++ b/testsys-launcher/testsys/src/delete.rs @@ -0,0 +1,88 @@ +use crate::error::Result; +use clap::Parser; +use futures::TryStreamExt; +use log::info; +use testsys_model::test_manager::{CrdState, CrdType, DeleteEvent, SelectionParams, TestManager}; + +/// Delete all tests and resources from a testsys cluster. +#[derive(Debug, Parser)] +pub(crate) struct Delete { + /// Only delete tests + #[clap(long)] + test: bool, + + /// Delete a particular test + #[clap(long)] + test_name: Option, + + /// Focus status on a particular arch + #[clap(long)] + arch: Option, + + /// Focus status on a particular variant + #[clap(long)] + variant: Option, + + /// Only delete passed tests + #[clap(long, conflicts_with_all=&["failed", "running"])] + passed: bool, + + /// Only delete failed tests + #[clap(long, conflicts_with_all=&["passed", "running"])] + failed: bool, + + /// Only CRD's that haven't finished + #[clap(long, conflicts_with_all=&["passed", "failed"])] + running: bool, +} + +impl Delete { + pub(crate) async fn run(self, client: TestManager) -> Result<()> { + let state = if self.running { + info!("Deleting all running tests and resources"); + Some(CrdState::NotFinished) + } else if self.passed { + info!("Deleting all passed tests"); + Some(CrdState::Passed) + } else if self.failed { + info!("Deleting all failed tests"); + Some(CrdState::Failed) + } else { + info!("Deleting all tests and resources"); + None + }; + let crd_type = self.test.then_some(CrdType::Test); + let mut labels = Vec::new(); + if let Some(test_name) = self.test_name { + labels.push(format!("testsys/test-name={test_name}")) + }; + if let Some(arch) = self.arch { + labels.push(format!("testsys/arch={arch}")) + }; + if let Some(variant) = self.variant { + labels.push(format!("testsys/variant={variant}")) + }; + + let mut stream = client + .delete( + &SelectionParams { + labels: Some(labels.join(",")), + state, + crd_type, + ..Default::default() + }, + !self.test, + ) + .await?; + + while let Some(delete) = stream.try_next().await? { + match delete { + DeleteEvent::Starting(crd) => println!("Starting delete for {}", crd.name()), + DeleteEvent::Deleted(crd) => println!("Delete finished for {}", crd.name()), + DeleteEvent::Failed(crd) => println!("Delete failed for {}", crd.name()), + } + } + info!("Delete finished"); + Ok(()) + } +} diff --git a/testsys-launcher/testsys/src/error.rs b/testsys-launcher/testsys/src/error.rs new file mode 100644 index 00000000..e965b815 --- /dev/null +++ b/testsys-launcher/testsys/src/error.rs @@ -0,0 +1,118 @@ +use aws_sdk_ec2::error::SdkError; +use aws_sdk_ec2::operation::describe_images::DescribeImagesError; +use snafu::Snafu; +use std::path::PathBuf; + +pub type Result = std::result::Result; + +#[derive(Debug, Snafu)] +#[snafu(visibility(pub(super)))] +pub enum Error { + // `error` must be used instead of `source` because the build function returns + // `std::error::Error` but not `std::error::Error + Sync + Send`. + #[snafu(display("Unable to build '{}': {}", what, source))] + Build { + what: String, + source: Box, + }, + + #[snafu(display("Unable to build datacenter credentials: {}", source))] + CredsBuild { + source: pubsys_config::vmware::Error, + }, + + #[snafu(display("Unable to build data center config: {}", source))] + DatacenterBuild { + source: pubsys_config::vmware::Error, + }, + + #[snafu(context(false), display("{}", source))] + DescribeImages { + #[snafu(source(from(SdkError, Box::new)))] + source: Box>, + }, + + #[snafu(display("Unable to read file '{}': {}", path.display(), source))] + File { + path: PathBuf, + source: std::io::Error, + }, + + #[snafu(context(false), display("Unable render templated yaml: {}", source))] + HandlebarsRender { source: handlebars::RenderError }, + + #[snafu( + context(false), + display("Unable create template from yaml: {}", source) + )] + HandlebarsTemplate { + #[snafu(source(from(handlebars::TemplateError, Box::new)))] + source: Box, + }, + + #[snafu(display("{}", what))] + Invalid { what: String }, + + #[snafu(display("{}: {}", what, source))] + IO { + what: String, + source: std::io::Error, + }, + + #[snafu(display("Unable to parse K8s version '{}'", version))] + K8sVersion { version: String }, + + #[snafu(display("{} was missing from {}", item, what))] + Missing { item: String, what: String }, + + #[snafu(context(false), display("{}", source))] + PubsysConfig { source: pubsys_config::Error }, + + #[snafu(display("Unable to create secret name for '{}': {}", secret_name, source))] + SecretName { + secret_name: String, + source: testsys_model::Error, + }, + + #[snafu(display("{}: {}", what, source))] + SerdeJson { + what: String, + source: serde_json::Error, + }, + + #[snafu(display("{}: {}", what, source))] + SerdeYaml { + what: String, + source: serde_yaml::Error, + }, + + #[snafu(context(false), display("{}", source))] + TestManager { + #[snafu(source(from(testsys_model::test_manager::Error, Box::new)))] + source: Box, + }, + + #[snafu(context(false), display("{}", source))] + TestsysConfig { source: testsys_config::Error }, + + #[snafu(display("{} is not supported.", what))] + Unsupported { what: String }, + + #[snafu(display("Unable to parse url from '{}': {}", url, source))] + #[allow(dead_code)] + UrlParse { + url: String, + source: url::ParseError, + }, + + #[snafu(display("Unable to create `Variant` from `{}`: {}", variant, source))] + Variant { + variant: String, + source: bottlerocket_variant::error::Error, + }, + + #[snafu(display("Error reading config: {}", source))] + VmwareConfig { + source: pubsys_config::vmware::Error, + }, +} diff --git a/testsys-launcher/testsys/src/install.rs b/testsys-launcher/testsys/src/install.rs new file mode 100644 index 00000000..817c4dbb --- /dev/null +++ b/testsys-launcher/testsys/src/install.rs @@ -0,0 +1,59 @@ +use crate::error::Result; +use crate::run::TestsysImages; +use clap::Parser; +use log::{info, trace}; +use std::path::PathBuf; +use testsys_config::TestConfig; +use testsys_model::test_manager::{ImageConfig, TestManager}; + +/// The install subcommand is responsible for putting all of the necessary components for testsys in +/// a k8s cluster. +#[derive(Debug, Parser)] +pub(crate) struct Install { + /// The path to `Test.toml` + #[arg(long, env = "TESTSYS_TEST_CONFIG_PATH")] + test_config_path: PathBuf, + + #[command(flatten)] + agent_images: TestsysImages, +} + +impl Install { + pub(crate) async fn run(self, client: TestManager) -> Result<()> { + // Use Test.toml or default + let test_config = TestConfig::from_path_or_default(&self.test_config_path)?; + + let test_opts = test_config.test.to_owned().unwrap_or_default(); + + let images = vec![ + Some(self.agent_images.into()), + Some(test_opts.testsys_images), + test_opts.testsys_image_registry.map(|registry| { + testsys_config::TestsysImages::new(registry, test_opts.testsys_image_tag) + }), + Some(testsys_config::TestsysImages::public_images()), + ] + .into_iter() + .flatten() + .fold(Default::default(), testsys_config::TestsysImages::merge); + + let controller_uri = images + .controller_image + .expect("The default controller image is missing."); + + trace!("Installing testsys using controller image '{controller_uri}'"); + + let controller_image = match images.testsys_agent_pull_secret { + Some(secret) => ImageConfig::WithCreds { + secret, + image: controller_uri, + }, + None => ImageConfig::Image(controller_uri), + }; + client.install(controller_image, true).await?; + + info!("testsys components were successfully installed."); + + Ok(()) + } +} diff --git a/testsys-launcher/testsys/src/logs.rs b/testsys-launcher/testsys/src/logs.rs new file mode 100644 index 00000000..c7151605 --- /dev/null +++ b/testsys-launcher/testsys/src/logs.rs @@ -0,0 +1,47 @@ +use crate::error::{self, Result}; +use clap::Parser; +use futures::{AsyncBufReadExt, TryStreamExt}; +use snafu::{OptionExt, ResultExt}; +use testsys_model::test_manager::{ResourceState, TestManager}; +use unescape::unescape; + +/// Stream the logs of an object from a testsys cluster. +#[derive(Debug, Parser)] +pub(crate) struct Logs { + /// The name of the test we want logs from. + #[clap(long, conflicts_with = "resource")] + test: Option, + + /// The name of the resource we want logs from. + #[clap(long, conflicts_with = "test", requires = "state")] + resource: Option, + + /// The resource state we want logs for (Creation, Destruction). + #[clap(long = "state", conflicts_with = "test")] + resource_state: Option, + + /// Follow logs + #[clap(long, short)] + follow: bool, +} + +impl Logs { + pub(crate) async fn run(self, client: TestManager) -> Result<()> { + match (self.test, self.resource, self.resource_state) { + (Some(test), None, None) => { + let mut logs = client.test_logs(test, self.follow).await?.lines(); + while let Some(line) = logs.try_next().await.context(error::IOSnafu { what: "Failed to read test logs".to_string()})? { + println!("{}", unescape(&line).context(error::InvalidSnafu { what: "Unable to unescape log string"})?); + } + } + (None, Some(resource), Some(state)) => { + let mut logs = client.resource_logs(resource, state, self.follow).await?.lines(); + while let Some(line) = logs.try_next().await.context(error::IOSnafu { what: "Failed to read test logs".to_string()})? { + println!("{}", unescape(&line).context(error::InvalidSnafu { what: "Unable to unescape log string"})?); + } + } + _ => return Err(error::Error::Invalid{what: "Invalid arguments were provided. Exactly one of `--test` or `--resource` must be given.".to_string()}), + }; + Ok(()) + } +} diff --git a/testsys-launcher/testsys/src/main.rs b/testsys-launcher/testsys/src/main.rs new file mode 100644 index 00000000..20c23522 --- /dev/null +++ b/testsys-launcher/testsys/src/main.rs @@ -0,0 +1,112 @@ +use clap::{Parser, Subcommand}; +use delete::Delete; +use env_logger::Builder; +use error::Result; +use install::Install; +use log::{debug, error, LevelFilter}; +use logs::Logs; +use restart_test::RestartTest; +use run::Run; +use secret::Add; +use status::Status; +use std::path::PathBuf; +use testsys_model::test_manager::TestManager; +use uninstall::Uninstall; + +mod aws_ecs; +mod aws_k8s; +mod aws_resources; +mod base64; +mod crds; +mod delete; +mod error; +mod install; +mod logs; +mod migration; +mod restart_test; +mod run; +mod secret; +mod sonobuoy; +mod status; +mod uninstall; +mod vmware_k8s; + +/// A program for running and controlling Bottlerocket tests in a Kubernetes cluster using +/// bottlerocket-test-system +#[derive(Parser, Debug)] +#[clap(about, long_about = None)] +struct TestsysArgs { + #[arg(global = true, long, default_value = "INFO")] + /// How much detail to log; from least to most: ERROR, WARN, INFO, DEBUG, TRACE + log_level: LevelFilter, + + /// Path to the kubeconfig file for the testsys cluster. Can also be passed with the KUBECONFIG + /// environment variable. + #[arg(long)] + kubeconfig: Option, + + #[command(subcommand)] + command: Command, +} + +impl TestsysArgs { + async fn run(self) -> Result<()> { + let client = match self.kubeconfig { + Some(path) => TestManager::new_from_kubeconfig_path(&path).await?, + None => TestManager::new().await?, + }; + match self.command { + Command::Run(run) => run.run(client).await?, + Command::Install(install) => install.run(client).await?, + Command::Delete(delete) => delete.run(client).await?, + Command::Status(status) => status.run(client).await?, + Command::Logs(logs) => logs.run(client).await?, + Command::RestartTest(restart_test) => restart_test.run(client).await?, + Command::Add(add) => add.run(client).await?, + Command::Uninstall(uninstall) => uninstall.run(client).await?, + }; + Ok(()) + } +} + +#[derive(Subcommand, Debug)] +enum Command { + // We need to box some commands because they require significantly more arguments than the other commands. + Install(Box), + Run(Box), + Delete(Delete), + Status(Status), + Logs(Logs), + RestartTest(RestartTest), + Add(Add), + Uninstall(Uninstall), +} + +#[tokio::main] +async fn main() { + let args = TestsysArgs::parse(); + init_logger(args.log_level); + debug!("{args:?}"); + if let Err(e) = args.run().await { + error!("{e}"); + std::process::exit(1); + } +} + +/// Initialize the logger with the value passed by `--log-level` (or its default) when the +/// `RUST_LOG` environment variable is not present. If present, the `RUST_LOG` environment variable +/// overrides `--log-level`/`level`. +fn init_logger(level: LevelFilter) { + match std::env::var(env_logger::DEFAULT_FILTER_ENV).ok() { + Some(_) => { + // RUST_LOG exists; env_logger will use it. + Builder::from_default_env().init(); + } + None => { + // RUST_LOG does not exist; use default log level for this crate only. + Builder::new() + .filter(Some(env!("CARGO_CRATE_NAME")), level) + .init(); + } + } +} diff --git a/testsys-launcher/testsys/src/migration.rs b/testsys-launcher/testsys/src/migration.rs new file mode 100644 index 00000000..6f8d5b84 --- /dev/null +++ b/testsys-launcher/testsys/src/migration.rs @@ -0,0 +1,114 @@ +use crate::crds::{MigrationDirection, MigrationInput}; +use crate::error::{self, Result}; +use bottlerocket_types::agent_config::MigrationConfig; +use maplit::btreemap; +use snafu::{OptionExt, ResultExt}; +use testsys_model::Test; + +/// Create a CRD for migrating Bottlerocket instances using SSM commands. +/// `aws_region_override` allows the region that's normally derived from the cluster resource CRD to be overridden +/// `instance_id_field_name` specifies the VM/Instance resource CRD field name for retrieving the instances IDs of the created instances +pub(crate) fn migration_crd( + migration_input: MigrationInput, + aws_region_override: Option, + instance_id_field_name: &str, +) -> Result { + let cluster_resource_name = migration_input + .cluster_crd_name + .as_ref() + .expect("A cluster name is required for migrations"); + + let labels = migration_input.crd_input.labels(btreemap! { + "testsys/type".to_string() => "migration".to_string(), + "testsys/cluster".to_string() => cluster_resource_name.to_string(), + "testsys/test-name".to_string() => format!( + "{}-{}", + cluster_resource_name, + migration_input.name_suffix.unwrap_or(migration_input.crd_input.test_flavor.as_str()) + ), + }); + + // Determine which version should be migrated to from `migration_input`. + let migration_version = match migration_input.migration_direction { + MigrationDirection::Upgrade => migration_input + .crd_input + .migrate_to_version + .as_ref() + .context(error::InvalidSnafu { + what: "The target migration version is required", + }), + MigrationDirection::Downgrade => migration_input + .crd_input + .starting_version + .as_ref() + .context(error::InvalidSnafu { + what: "The starting migration version is required", + }), + }?; + + // Construct the migration CRD. + let mut migration_config = MigrationConfig::builder(); + + // Use the specified AWS region for the migration test. + // If no region is specified, derive the appropriate region based on the region of the + // cluster resource CRD (assuming it's an ECS or EKS cluster). + if let Some(aws_region) = aws_region_override { + migration_config.aws_region(aws_region) + } else { + migration_config.aws_region_template(cluster_resource_name, "region") + }; + + migration_config + .instance_ids_template( + migration_input + .bottlerocket_crd_name + .as_ref() + .unwrap_or(cluster_resource_name), + instance_id_field_name, + ) + .migrate_to_version(migration_version) + .tuf_repo(migration_input.crd_input.tuf_repo_config()) + .assume_role(migration_input.crd_input.config.agent_role.clone()) + .set_resources(Some( + vec![cluster_resource_name.to_owned()] + .into_iter() + .chain(migration_input.bottlerocket_crd_name.iter().cloned()) + .collect(), + )) + .set_depends_on(Some(migration_input.prev_tests)) + .image( + migration_input + .crd_input + .images + .migration_test_agent_image + .as_ref() + .expect("Missing default image for migration test agent"), + ) + .set_image_pull_secret( + migration_input + .crd_input + .images + .testsys_agent_pull_secret + .to_owned(), + ) + .keep_running( + migration_input + .crd_input + .config + .dev + .keep_tests_running + .unwrap_or(false), + ) + .set_secrets(Some(migration_input.crd_input.config.secrets.to_owned())) + .set_labels(Some(labels)) + .build(format!( + "{}-{}", + cluster_resource_name, + migration_input + .name_suffix + .unwrap_or(migration_input.crd_input.test_flavor.as_str()) + )) + .context(error::BuildSnafu { + what: "migration CRD", + }) +} diff --git a/testsys-launcher/testsys/src/restart_test.rs b/testsys-launcher/testsys/src/restart_test.rs new file mode 100644 index 00000000..85f4fbac --- /dev/null +++ b/testsys-launcher/testsys/src/restart_test.rs @@ -0,0 +1,18 @@ +use crate::error::Result; +use clap::Parser; +use testsys_model::test_manager::TestManager; + +/// Restart a test. This will delete the test object from the testsys cluster and replace it with +/// a new, identical test object with a clean state. +#[derive(Debug, Parser)] +pub(crate) struct RestartTest { + /// The name of the test to be restarted. + #[clap()] + test_name: String, +} + +impl RestartTest { + pub(crate) async fn run(self, client: TestManager) -> Result<()> { + Ok(client.restart_test(&self.test_name).await?) + } +} diff --git a/testsys-launcher/testsys/src/run.rs b/testsys-launcher/testsys/src/run.rs new file mode 100644 index 00000000..d23ac5a6 --- /dev/null +++ b/testsys-launcher/testsys/src/run.rs @@ -0,0 +1,601 @@ +use crate::aws_ecs::AwsEcsCreator; +use crate::aws_k8s::AwsK8sCreator; +use crate::base64; +use crate::crds::{CrdCreator, CrdInput}; +use crate::error; +use crate::error::Result; + +use crate::vmware_k8s::VmwareK8sCreator; +use bottlerocket_variant::Variant; +use clap::Parser; +use log::{debug, info}; +use pubsys_config::vmware::{ + Datacenter, DatacenterBuilder, DatacenterCreds, DatacenterCredsBuilder, DatacenterCredsConfig, + VMWARE_CREDS_PATH, +}; +use pubsys_config::InfraConfig; +use serde::{Deserialize, Serialize}; +use serde_plain::{derive_display_from_serialize, derive_fromstr_from_deserialize}; +use snafu::{OptionExt, ResultExt}; +use std::fs::read_to_string; +use std::path::PathBuf; +use std::str::FromStr; +use testsys_config::{GenericVariantConfig, ResourceAgentType, TestConfig}; +use testsys_model::test_manager::TestManager; +use testsys_model::SecretName; + +/// Run a set of tests for a given arch and variant +#[derive(Debug, Parser)] +pub(crate) struct Run { + /// The type of test to run. Options are `quick` and `conformance`. + test_flavor: TestType, + + /// The architecture to test. Either x86_64 or aarch64. + #[arg(long, env = "BUILDSYS_ARCH")] + arch: String, + + /// The variant to test + #[arg(long, env = "BUILDSYS_VARIANT")] + variant: String, + + /// The path to `Infra.toml` + #[arg(long, env = "PUBLISH_INFRA_CONFIG_PATH")] + infra_config_path: PathBuf, + + /// The path to `Test.toml` + #[arg(long, env = "TESTSYS_TEST_CONFIG_PATH")] + test_config_path: PathBuf, + + /// The path to the `tests` directory + #[arg(long, env = "TESTSYS_TESTS_DIR")] + tests_directory: PathBuf, + + /// The path to the EKS-A management cluster kubeconfig for vSphere or metal K8s cluster creation + #[arg(long, env = "TESTSYS_MGMT_CLUSTER_KUBECONFIG")] + mgmt_cluster_kubeconfig: Option, + + /// Use this named repo infrastructure from Infra.toml for upgrade/downgrade testing. + #[arg(long, env = "PUBLISH_REPO")] + repo: Option, + + /// The name of the vSphere data center in `Infra.toml` that should be used for testing + /// If no data center is provided, the first one in `vmware.datacenters` will be used + #[arg(long, env = "TESTSYS_DATACENTER")] + datacenter: Option, + + /// The name of the VMware OVA that should be used for testing + #[arg(long, env = "BUILDSYS_OVA")] + ova_name: Option, + + /// The name of the image that should be used for Bare Metal testing + #[arg(long, env = "BUILDSYS_NAME_FULL")] + image_name: Option, + + /// The path to `amis.json` + #[arg(long, env = "AMI_INPUT")] + ami_input: Option, + + /// Override for the region the tests should be run in. If none is provided the first region in + /// Infra.toml will be used. This is the region that the aws client is created with for testing + /// and resource agents. + #[arg(long, env = "TESTSYS_TARGET_REGION")] + target_region: Option, + + #[arg(long, env = "BUILDSYS_VERSION_BUILD")] + build_id: Option, + + #[command(flatten)] + agent_images: TestsysImages, + + #[command(flatten)] + config: CliConfig, + + // Migrations + /// Override the starting image used for migrations. The image will be pulled from available + /// amis in the users account if no override is provided. + #[arg(long, env = "TESTSYS_STARTING_IMAGE_ID")] + starting_image_id: Option, + + /// The starting version for migrations. This is required for all migrations tests. + /// This is the version that will be created and migrated to `migration-target-version`. + #[arg(long, env = "TESTSYS_STARTING_VERSION")] + migration_starting_version: Option, + + /// The commit id of the starting version for migrations. This is required for all migrations + /// tests unless `starting-image-id` is provided. This is the version that will be created and + /// migrated to `migration-target-version`. + #[arg(long, env = "TESTSYS_STARTING_COMMIT")] + migration_starting_commit: Option, + + /// The target version for migrations. This is required for all migration tests. This is the + /// version that will be migrated to. + #[arg(long, env = "BUILDSYS_VERSION_IMAGE")] + migration_target_version: Option, + + /// The template file that should be used for custom testing. + #[arg(long = "template-file", short = 'f')] + custom_crd_template: Option, +} + +/// This is a CLI parsable version of `testsys_config::GenericVariantConfig`. +#[derive(Debug, Parser)] +struct CliConfig { + /// The repo containing images necessary for conformance testing. It may be omitted to use the + /// default conformance image registry. + #[arg(long, env = "TESTSYS_CONFORMANCE_REGISTRY")] + conformance_registry: Option, + + /// The name of the cluster for resource agents (EKS resource agent, ECS resource agent). Note: + /// This is not the name of the `testsys cluster` this is the name of the cluster that tests + /// should be run on. If no cluster name is provided, the bottlerocket cluster + /// naming convention `{{arch}}-{{variant}}` will be used. + #[arg(long, env = "TESTSYS_TARGET_CLUSTER_NAME")] + target_cluster_name: Option, + + /// The sonobuoy image that should be used for conformance testing. It may be omitted to use the default + /// sonobuoy image. + #[arg(long, env = "TESTSYS_SONOBUOY_IMAGE")] + sonobuoy_image: Option, + + /// The image that should be used for conformance testing. It may be omitted to use the default + /// testing image. + #[arg(long, env = "TESTSYS_CONFORMANCE_IMAGE")] + conformance_image: Option, + + /// The role that should be assumed by the agents + #[arg(long, env = "TESTSYS_ASSUME_ROLE")] + assume_role: Option, + + /// Specify the instance type that should be used. This is only applicable for aws-* variants. + /// It can be omitted for non-aws variants and can be omitted to use default instance types. + #[arg(long, env = "TESTSYS_INSTANCE_TYPE")] + instance_type: Option, + + /// Add secrets to the testsys agents (`--secret awsCredentials=my-secret`) + #[arg(long, short, value_parser = parse_key_val, number_of_values = 1)] + secret: Vec<(String, SecretName)>, + + /// The endpoint IP to reserve for the vSphere control plane VMs when creating a K8s cluster + #[arg(long, env = "TESTSYS_CONTROL_PLANE_ENDPOINT")] + pub control_plane_endpoint: Option, + + /// Specify the path to the userdata that should be added for Bottlerocket launch + #[arg(long, env = "TESTSYS_USERDATA")] + pub userdata: Option, + + /// Specify the method that should be used to launch instances + #[arg(long, env = "TESTSYS_RESOURCE_AGENT")] + pub resource_agent_type: Option, + + /// A set of workloads that should be run for a workload test (--workload my-workload=) + #[arg(long = "workload", value_parser = parse_workloads, number_of_values = 1)] + pub workloads: Vec<(String, String)>, + + /// The directory containing Bottlerocket images. For metal, this is the directory containing + /// gzipped images. + #[arg(long)] + pub os_image_dir: Option, + + /// The hardware that should be used for provisioning Bottlerocket. For metal, this is the + /// hardware csv that is passed to EKS Anywhere. + #[arg(long)] + pub hardware_csv: Option, +} + +impl From for GenericVariantConfig { + fn from(val: CliConfig) -> Self { + GenericVariantConfig { + cluster_names: val.target_cluster_name.into_iter().collect(), + instance_type: val.instance_type, + resource_agent_type: val.resource_agent_type, + block_device_mapping: Default::default(), + secrets: val.secret.into_iter().collect(), + agent_role: val.assume_role, + sonobuoy_image: val.sonobuoy_image, + conformance_image: val.conformance_image, + conformance_registry: val.conformance_registry, + control_plane_endpoint: val.control_plane_endpoint, + userdata: val.userdata, + os_image_dir: val.os_image_dir, + hardware_csv: val.hardware_csv, + dev: Default::default(), + workloads: val.workloads.into_iter().collect(), + } + } +} + +impl Run { + pub(crate) async fn run(self, client: TestManager) -> Result<()> { + // agent config (eventually with configuration) + let variant = Variant::new(&self.variant).context(error::VariantSnafu { + variant: self.variant, + })?; + debug!("Using variant '{variant}'"); + + // Use Test.toml or default + let test_config = TestConfig::from_path_or_default(&self.test_config_path)?; + + let test_opts = test_config.test.to_owned().unwrap_or_default(); + + let (variant_config, test_type) = test_config.reduced_config( + &variant, + &self.arch, + Some(self.config.into()), + &self.test_flavor.to_string(), + ); + let resolved_test_type = TestType::from_str(&test_type) + .expect("All unrecognized test type become `TestType::Custom`"); + + // If a lock file exists, use that, otherwise use Infra.toml or default + let infra_config = InfraConfig::from_path_or_lock(&self.infra_config_path, true)?; + + let repo_config = infra_config + .repo + .unwrap_or_default() + .remove( + &self + .repo + .or(test_opts.repo) + .unwrap_or_else(|| "default".to_string()), + ) + .unwrap_or_default(); + + let images = vec![ + Some(self.agent_images.into()), + Some(test_opts.testsys_images), + test_opts.testsys_image_registry.map(|registry| { + testsys_config::TestsysImages::new(registry, test_opts.testsys_image_tag) + }), + Some(testsys_config::TestsysImages::public_images()), + ] + .into_iter() + .flatten() + .fold(Default::default(), testsys_config::TestsysImages::merge); + + // The `CrdCreator` is responsible for creating crds for the given architecture and variant. + let crd_creator: Box = match variant.family() { + "aws-k8s" => { + debug!("Using family 'aws-k8s'"); + let aws_config = infra_config.aws.unwrap_or_default(); + let region = aws_config + .regions + .front() + .map(String::to_string) + .unwrap_or_else(|| "us-west-2".to_string()); + Box::new(AwsK8sCreator { + region, + ami_input: self.ami_input.context(error::InvalidSnafu { + what: "amis.json is required. You may need to run `cargo make ami`", + })?, + migrate_starting_commit: self.migration_starting_commit, + }) + } + "aws-ecs" => { + debug!("Using family 'aws-ecs'"); + let aws_config = infra_config.aws.unwrap_or_default(); + let region = aws_config + .regions + .front() + .map(String::to_string) + .unwrap_or_else(|| "us-west-2".to_string()); + Box::new(AwsEcsCreator { + region, + ami_input: self.ami_input.context(error::InvalidSnafu { + what: "amis.json is required. You may need to run `cargo make ami`", + })?, + migrate_starting_commit: self.migration_starting_commit, + }) + } + "vmware-k8s" => { + debug!("Using family 'vmware-k8s'"); + let aws_config = infra_config.aws.unwrap_or_default(); + let region = aws_config + .regions + .front() + .map(String::to_string) + .unwrap_or_else(|| "us-west-2".to_string()); + let vmware_config = infra_config.vmware.unwrap_or_default(); + let dc_env = DatacenterBuilder::from_env(); + let dc_common = vmware_config.common.as_ref(); + let dc_config = self + .datacenter + .as_ref() + .or_else(|| vmware_config.datacenters.first()) + .and_then(|datacenter| vmware_config.datacenter.get(datacenter)); + + let datacenter: Datacenter = dc_env + .take_missing_from(dc_config) + .take_missing_from(dc_common) + .build() + .context(error::DatacenterBuildSnafu)?; + + let vsphere_secret = if !variant_config.secrets.contains_key("vsphereCredentials") { + info!("Creating vSphere secret, 'vspherecreds'"); + let creds_env = DatacenterCredsBuilder::from_env(); + let creds_file = if let Some(ref creds_file) = *VMWARE_CREDS_PATH { + if creds_file.exists() { + info!("Using vSphere credentials file at {}", creds_file.display()); + DatacenterCredsConfig::from_path(creds_file) + .context(error::VmwareConfigSnafu)? + } else { + info!( + "vSphere credentials file not found, will attempt to use environment" + ); + DatacenterCredsConfig::default() + } + } else { + info!("Unable to determine vSphere credentials file location, will attempt to use environment"); + DatacenterCredsConfig::default() + }; + let dc_creds = creds_file.datacenter.get(&datacenter.datacenter); + let creds: DatacenterCreds = creds_env + .take_missing_from(dc_creds) + .build() + .context(error::CredsBuildSnafu)?; + + let secret_name = + SecretName::new("vspherecreds").context(error::SecretNameSnafu { + secret_name: "vspherecreds", + })?; + client + .create_secret( + &secret_name, + vec![ + ("username".to_string(), creds.username), + ("password".to_string(), creds.password), + ], + ) + .await?; + Some(("vsphereCredentials".to_string(), secret_name)) + } else { + None + }; + + let mgmt_cluster_kubeconfig = + self.mgmt_cluster_kubeconfig.context(error::InvalidSnafu { + what: "A management cluster kubeconfig is required for VMware testing", + })?; + let encoded_kubeconfig = base64::encode( + read_to_string(&mgmt_cluster_kubeconfig).context(error::FileSnafu { + path: mgmt_cluster_kubeconfig, + })?, + ); + + Box::new(VmwareK8sCreator { + region, + ova_name: self.ova_name.context(error::InvalidSnafu { + what: "An OVA name is required for VMware testing.", + })?, + datacenter, + encoded_mgmt_cluster_kubeconfig: encoded_kubeconfig, + creds: vsphere_secret, + }) + } + "metal-k8s" => { + return Err(error::Error::Unsupported { + what: "metal-k8s variant is deprecated and no longer supported".to_string(), + }) + } + unsupported => { + return Err(error::Error::Unsupported { + what: unsupported.to_string(), + }) + } + }; + + let crd_input = CrdInput { + client: &client, + arch: self.arch, + variant, + build_id: self.build_id, + config: variant_config, + repo_config, + starting_version: self.migration_starting_version, + migrate_to_version: self.migration_target_version, + starting_image_id: self.starting_image_id, + test_type: resolved_test_type.clone(), + test_flavor: self.test_flavor.to_string(), + images, + tests_directory: self.tests_directory, + }; + + let crds = match &resolved_test_type { + TestType::Known(resolved_test_type) => { + crd_creator + .create_crds(resolved_test_type, &crd_input) + .await? + } + TestType::Custom(resolved_test_type) => { + crd_creator + .create_custom_crds( + resolved_test_type, + &crd_input, + self.custom_crd_template.to_owned(), + ) + .await? + } + }; + + debug!("Adding crds to testsys cluster"); + for crd in crds { + let crd = client.create_object(crd).await?; + info!("Successfully added '{}'", crd.name().unwrap()); + } + + Ok(()) + } +} + +fn parse_key_val(s: &str) -> Result<(String, SecretName)> { + let mut iter = s.splitn(2, '='); + let key = iter.next().context(error::InvalidSnafu { + what: "Key is missing", + })?; + let value = iter.next().context(error::InvalidSnafu { + what: "Value is missing", + })?; + Ok(( + key.to_string(), + SecretName::new(value).context(error::SecretNameSnafu { secret_name: value })?, + )) +} + +fn parse_workloads(s: &str) -> Result<(String, String)> { + let mut iter = s.splitn(2, '='); + let key = iter.next().context(error::InvalidSnafu { + what: "Key is missing", + })?; + let value = iter.next().context(error::InvalidSnafu { + what: "Value is missing", + })?; + Ok((key.to_string(), value.to_string())) +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "lowercase")] +pub enum KnownTestType { + /// Conformance testing is a full integration test that asserts that Bottlerocket is working for + /// customer workloads. For k8s variants, for example, this will run the full suite of sonobuoy + /// conformance tests. + Conformance, + /// Run a quick test that ensures a basic workload can run on Bottlerocket. For example, on k8s + /// variance this will run sonobuoy in "quick" mode. For ECS variants, this will run a simple + /// ECS task. + Quick, + /// Migration testing ensures that all bottlerocket migrations work as expected. Instances will + /// be created at the starting version, migrated to the target version and back to the starting + /// version with validation testing. + Migration, + /// Workload testing is used to test specific workloads on a set of Bottlerocket nodes. + Workload, +} + +/// If a test type is one that is supported by TestSys it will be created as `Known(KnownTestType)`. +/// All other test types will be stored as `Custom()`. +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(untagged)] +pub(crate) enum TestType { + Known(KnownTestType), + Custom(String), +} + +derive_fromstr_from_deserialize!(TestType); +derive_display_from_serialize!(TestType); +derive_display_from_serialize!(KnownTestType); + +/// This is a CLI parsable version of `testsys_config::TestsysImages` +#[derive(Debug, Parser)] +pub(crate) struct TestsysImages { + /// EKS resource agent URI. If not provided the latest released resource agent will be used. + #[arg( + long = "eks-resource-agent-image", + env = "TESTSYS_EKS_RESOURCE_AGENT_IMAGE" + )] + pub(crate) eks_resource: Option, + + /// ECS resource agent URI. If not provided the latest released resource agent will be used. + #[arg( + long = "ecs-resource-agent-image", + env = "TESTSYS_ECS_RESOURCE_AGENT_IMAGE" + )] + pub(crate) ecs_resource: Option, + + /// vSphere cluster resource agent URI. If not provided the latest released resource agent will be used. + #[arg( + long = "vsphere-k8s-cluster-resource-agent-image", + env = "TESTSYS_VSPHERE_K8S_CLUSTER_RESOURCE_AGENT_IMAGE" + )] + pub(crate) vsphere_k8s_cluster_resource: Option, + + /// Bare Metal cluster resource agent URI. If not provided the latest released resource agent will be used. + #[arg( + long = "metal-k8s-cluster-resource-agent-image", + env = "TESTSYS_METAL_K8S_CLUSTER_RESOURCE_AGENT_IMAGE" + )] + pub(crate) metal_k8s_cluster_resource: Option, + + /// EC2 resource agent URI. If not provided the latest released resource agent will be used. + #[arg( + long = "ec2-resource-agent-image", + env = "TESTSYS_EC2_RESOURCE_AGENT_IMAGE" + )] + pub(crate) ec2_resource: Option, + + /// EC2 Karpenter resource agent URI. If not provided the latest released resource agent will be used. + #[arg( + long = "ec2-resource-agent-image", + env = "TESTSYS_EC2_KARPENTER_RESOURCE_AGENT_IMAGE" + )] + pub(crate) ec2_karpenter_resource: Option, + + /// vSphere VM resource agent URI. If not provided the latest released resource agent will be used. + #[arg( + long = "vsphere-vm-resource-agent-image", + env = "TESTSYS_VSPHERE_VM_RESOURCE_AGENT_IMAGE" + )] + pub(crate) vsphere_vm_resource: Option, + + /// Sonobuoy test agent URI. If not provided the latest released test agent will be used. + #[arg( + long = "sonobuoy-test-agent-image", + env = "TESTSYS_SONOBUOY_TEST_AGENT_IMAGE" + )] + pub(crate) sonobuoy_test: Option, + + /// ECS test agent URI. If not provided the latest released test agent will be used. + #[arg(long = "ecs-test-agent-image", env = "TESTSYS_ECS_TEST_AGENT_IMAGE")] + pub(crate) ecs_test: Option, + + /// Migration test agent URI. If not provided the latest released test agent will be used. + #[arg( + long = "migration-test-agent-image", + env = "TESTSYS_MIGRATION_TEST_AGENT_IMAGE" + )] + pub(crate) migration_test: Option, + + /// K8s workload agent URI. If not provided the latest released test agent will be used. + #[arg( + long = "k8s-workload-agent-image", + env = "TESTSYS_K8S_WORKLOAD_AGENT_IMAGE" + )] + pub(crate) k8s_workload: Option, + + /// ECS workload agent URI. If not provided the latest released test agent will be used. + #[arg( + long = "ecs-workload-agent-image", + env = "TESTSYS_ECS_WORKLOAD_AGENT_IMAGE" + )] + pub(crate) ecs_workload: Option, + + /// TestSys controller URI. If not provided the latest released controller will be used. + #[arg(long = "controller-image", env = "TESTSYS_CONTROLLER_IMAGE")] + pub(crate) controller_uri: Option, + + /// Images pull secret. This is the name of a Kubernetes secret that will be used to + /// pull the container image from a private registry. For example, if you created a pull secret + /// with `kubectl create secret docker-registry regcred` then you would pass + /// `--images-pull-secret regcred`. + #[arg(long = "images-pull-secret", env = "TESTSYS_IMAGES_PULL_SECRET")] + pub(crate) secret: Option, +} + +impl From for testsys_config::TestsysImages { + fn from(val: TestsysImages) -> Self { + testsys_config::TestsysImages { + eks_resource_agent_image: val.eks_resource, + ecs_resource_agent_image: val.ecs_resource, + vsphere_k8s_cluster_resource_agent_image: val.vsphere_k8s_cluster_resource, + metal_k8s_cluster_resource_agent_image: val.metal_k8s_cluster_resource, + ec2_resource_agent_image: val.ec2_resource, + ec2_karpenter_resource_agent_image: val.ec2_karpenter_resource, + vsphere_vm_resource_agent_image: val.vsphere_vm_resource, + sonobuoy_test_agent_image: val.sonobuoy_test, + ecs_test_agent_image: val.ecs_test, + migration_test_agent_image: val.migration_test, + k8s_workload_agent_image: val.k8s_workload, + ecs_workload_agent_image: val.ecs_workload, + controller_image: val.controller_uri, + testsys_agent_pull_secret: val.secret, + } + } +} diff --git a/testsys-launcher/testsys/src/secret.rs b/testsys-launcher/testsys/src/secret.rs new file mode 100644 index 00000000..6343c163 --- /dev/null +++ b/testsys-launcher/testsys/src/secret.rs @@ -0,0 +1,118 @@ +use crate::error::{self, Result}; +use clap::Parser; +use snafu::OptionExt; +use testsys_model::test_manager::TestManager; +use testsys_model::SecretName; + +/// Add a testsys object to the testsys cluster. +#[derive(Debug, Parser)] +pub(crate) struct Add { + #[command(subcommand)] + command: AddCommand, +} + +#[derive(Debug, Parser)] +enum AddCommand { + /// Add a secret to the testsys cluster. + Secret(AddSecret), +} + +impl Add { + pub(crate) async fn run(self, client: TestManager) -> Result<()> { + match self.command { + AddCommand::Secret(add_secret) => add_secret.run(client).await, + } + } +} + +/// Add a secret to the cluster. +#[derive(Debug, Parser)] +pub(crate) struct AddSecret { + #[command(subcommand)] + command: Command, +} + +#[derive(Debug, Parser)] +enum Command { + /// Create a secret for image pulls. + Image(AddSecretImage), + /// Create a secret from key value pairs. + Map(AddSecretMap), +} + +impl AddSecret { + pub(crate) async fn run(self, client: TestManager) -> Result<()> { + match self.command { + Command::Image(add_secret_image) => add_secret_image.run(client).await, + Command::Map(add_secret_map) => add_secret_map.run(client).await, + } + } +} + +/// Add a `Secret` with key value pairs. +#[derive(Debug, Parser)] +pub(crate) struct AddSecretMap { + /// Name of the secret + #[arg(short, long)] + name: SecretName, + + /// Key value pairs for secrets. (Key=value) + #[arg(value_parser = parse_key_val)] + args: Vec<(String, String)>, +} + +impl AddSecretMap { + pub(crate) async fn run(self, client: TestManager) -> Result<()> { + client.create_secret(&self.name, self.args).await?; + println!("Successfully added '{}' to secrets.", self.name); + Ok(()) + } +} + +fn parse_key_val(s: &str) -> Result<(String, String)> { + let mut iter = s.splitn(2, '='); + let key = iter.next().context(error::InvalidSnafu { + what: "Key is missing", + })?; + let value = iter.next().context(error::InvalidSnafu { + what: "Value is missing", + })?; + Ok((key.to_string(), value.to_string())) +} + +/// Add a secret to the testsys cluster for image pulls. +#[derive(Debug, Parser)] +pub(crate) struct AddSecretImage { + /// Controller image pull username + #[arg(long, short = 'u')] + pull_username: String, + + /// Controller image pull password + #[arg(long, short = 'p')] + pull_password: String, + + /// Image uri + #[arg(long = "image-uri", short)] + image_uri: String, + + /// Controller image uri + #[arg(long, short = 'n')] + secret_name: String, +} + +impl AddSecretImage { + pub(crate) async fn run(self, client: TestManager) -> Result<()> { + client + .create_image_pull_secret( + &self.secret_name, + &self.pull_username, + &self.pull_password, + &self.image_uri, + ) + .await?; + + println!("The secret was added."); + + Ok(()) + } +} diff --git a/testsys-launcher/testsys/src/sonobuoy.rs b/testsys-launcher/testsys/src/sonobuoy.rs new file mode 100644 index 00000000..37a4208c --- /dev/null +++ b/testsys-launcher/testsys/src/sonobuoy.rs @@ -0,0 +1,185 @@ +use crate::base64; +use crate::crds::TestInput; +use crate::error::{self, Result}; +use crate::run::KnownTestType; +use bottlerocket_types::agent_config::{ + SonobuoyConfig, SonobuoyMode, WorkloadConfig, WorkloadTest, +}; +use maplit::btreemap; +use snafu::ResultExt; +use std::fmt::Display; +use testsys_model::Test; + +/// Create a Sonobuoy CRD for K8s conformance and quick testing. +pub(crate) fn sonobuoy_crd(test_input: TestInput) -> Result { + let cluster_resource_name = test_input + .cluster_crd_name + .as_ref() + .expect("A cluster name is required for sonobuoy testing"); + let bottlerocket_resource_name = test_input.bottlerocket_crd_name; + let sonobuoy_mode = match test_input.test_type { + KnownTestType::Conformance => SonobuoyMode::CertifiedConformance, + KnownTestType::Quick | KnownTestType::Migration | KnownTestType::Workload => { + SonobuoyMode::Quick + } + }; + + let labels = test_input.crd_input.labels(btreemap! { + "testsys/type".to_string() => test_input.test_type.to_string(), + "testsys/flavor".to_string() => test_input.crd_input.test_flavor.clone(), + "testsys/cluster".to_string() => cluster_resource_name.to_string(), + "testsys/test-name".to_string() => format!( + "{}-{}", + cluster_resource_name, + test_input.name_suffix.unwrap_or(test_input.crd_input.test_flavor.as_str()) + ), + }); + + SonobuoyConfig::builder() + .set_resources(Some(bottlerocket_resource_name.iter().cloned().collect())) + .resources(cluster_resource_name) + .set_depends_on(Some(test_input.prev_tests)) + .set_retries(Some(5)) + .image( + test_input + .crd_input + .images + .sonobuoy_test_agent_image + .to_owned() + .expect("The default Sonobuoy testing image is missing"), + ) + .set_image_pull_secret( + test_input + .crd_input + .images + .testsys_agent_pull_secret + .to_owned(), + ) + .keep_running( + test_input + .crd_input + .config + .dev + .keep_tests_running + .unwrap_or(false), + ) + .kubeconfig_base64_template(cluster_resource_name, "encodedKubeconfig") + .plugin("e2e") + .mode(sonobuoy_mode) + .e2e_repo_config_base64( + test_input + .crd_input + .config + .conformance_registry + .to_owned() + .map(e2e_repo_config_base64), + ) + .sonobuoy_image(test_input.crd_input.config.sonobuoy_image.to_owned()) + .kube_conformance_image(test_input.crd_input.config.conformance_image.to_owned()) + .assume_role(test_input.crd_input.config.agent_role.to_owned()) + .set_secrets(Some(test_input.crd_input.config.secrets.to_owned())) + .set_labels(Some(labels)) + .build(format!( + "{}-{}", + cluster_resource_name, + test_input + .name_suffix + .unwrap_or(test_input.crd_input.test_flavor.as_str()) + )) + .context(error::BuildSnafu { + what: "Sonobuoy CRD", + }) +} + +/// Create a workload CRD for K8s testing. +pub(crate) fn workload_crd(test_input: TestInput) -> Result { + let cluster_resource_name = test_input + .cluster_crd_name + .as_ref() + .expect("A cluster name is required for migrations"); + let bottlerocket_resource_name = test_input + .bottlerocket_crd_name + .as_ref() + .expect("A cluster name is required for migrations"); + + let labels = test_input.crd_input.labels(btreemap! { + "testsys/type".to_string() => test_input.test_type.to_string(), + "testsys/cluster".to_string() => cluster_resource_name.to_string(), + "testsys/test-name".to_string() => format!( + "{}-{}", + cluster_resource_name, + test_input.name_suffix.unwrap_or("test") + ), + }); + let gpu = test_input.crd_input.variant.variant_flavor() == Some("nvidia"); + let plugins: Vec<_> = test_input + .crd_input + .config + .workloads + .iter() + .map(|(name, image)| WorkloadTest { + name: name.to_string(), + image: image.to_string(), + gpu, + }) + .collect(); + if plugins.is_empty() { + return Err(error::Error::Invalid { + what: "There were no plugins specified in the workload test. + Workloads can be specified in `Test.toml` or via the command line." + .to_string(), + }); + } + + WorkloadConfig::builder() + .resources(bottlerocket_resource_name) + .resources(cluster_resource_name) + .set_depends_on(Some(test_input.prev_tests)) + .set_retries(Some(5)) + .image( + test_input + .crd_input + .images + .k8s_workload_agent_image + .to_owned() + .expect("The default K8s workload testing image is missing"), + ) + .set_image_pull_secret( + test_input + .crd_input + .images + .testsys_agent_pull_secret + .to_owned(), + ) + .keep_running(true) + .kubeconfig_base64_template(cluster_resource_name, "encodedKubeconfig") + .tests(plugins) + .assume_role(test_input.crd_input.config.agent_role.to_owned()) + .set_secrets(Some(test_input.crd_input.config.secrets.to_owned())) + .set_labels(Some(labels)) + .build(format!( + "{}{}", + cluster_resource_name, + test_input.name_suffix.unwrap_or("-test") + )) + .context(error::BuildSnafu { + what: "Workload CRD", + }) +} + +fn e2e_repo_config_base64(e2e_registry: S) -> String +where + S: Display, +{ + base64::encode(format!( + r#"buildImageRegistry: {e2e_registry} +dockerGluster: {e2e_registry} +dockerLibraryRegistry: {e2e_registry} +e2eRegistry: {e2e_registry} +e2eVolumeRegistry: {e2e_registry} +gcRegistry: {e2e_registry} +gcEtcdRegistry: {e2e_registry} +promoterE2eRegistry: {e2e_registry} +sigStorageRegistry: {e2e_registry}"# + )) +} diff --git a/testsys-launcher/testsys/src/status.rs b/testsys-launcher/testsys/src/status.rs new file mode 100644 index 00000000..83bbb4ae --- /dev/null +++ b/testsys-launcher/testsys/src/status.rs @@ -0,0 +1,130 @@ +use crate::error::{self, Result}; +use clap::Parser; +use log::{debug, info}; +use serde::Deserialize; +use serde_plain::derive_fromstr_from_deserialize; +use snafu::ResultExt; +use testsys_model::test_manager::{CrdState, CrdType, SelectionParams, StatusColumn, TestManager}; + +/// Check the status of testsys objects. +#[derive(Debug, Parser)] +pub(crate) struct Status { + /// Configure the output of the command (json, narrow, wide). + #[arg(long, short = 'o')] + output: Option, + + /// Focus status on a particular arch + #[arg(long)] + arch: Option, + + /// Focus status on a particular variant + #[arg(long)] + variant: Option, + + /// Only show tests + #[arg(long)] + test: bool, + + /// Only show passed tests + #[arg(long, conflicts_with_all=&["failed", "running"])] + passed: bool, + + /// Only show failed tests + #[arg(long, conflicts_with_all=&["passed", "running"])] + failed: bool, + + /// Only CRD's that haven't finished + #[arg(long, conflicts_with_all=&["passed", "failed"])] + running: bool, +} + +impl Status { + pub(crate) async fn run(self, client: TestManager) -> Result<()> { + let state = if self.running { + Some(CrdState::NotFinished) + } else if self.passed { + Some(CrdState::Passed) + } else if self.failed { + Some(CrdState::Failed) + } else { + None + }; + let crd_type = self.test.then_some(CrdType::Test); + let mut labels = Vec::new(); + if let Some(arch) = self.arch { + labels.push(format!("testsys/arch={arch}")) + }; + if let Some(variant) = self.variant { + labels.push(format!("testsys/variant={variant}")) + }; + let mut status = client + .status(&SelectionParams { + labels: Some(labels.join(",")), + state, + crd_type, + ..Default::default() + }) + .await?; + + status.add_column(StatusColumn::name()); + status.add_column(StatusColumn::crd_type()); + status.add_column(StatusColumn::state()); + status.add_column(StatusColumn::passed()); + status.add_column(StatusColumn::failed()); + status.add_column(StatusColumn::skipped()); + + match self.output { + Some(StatusOutput::Json) => { + info!( + "{}", + serde_json::to_string_pretty(&status).context(error::SerdeJsonSnafu { + what: "Could not create string from status." + })? + ); + return Ok(()); + } + Some(StatusOutput::Narrow) => (), + None => { + status.new_column("BUILD ID", |crd| { + crd.labels() + .get("testsys/build-id") + .cloned() + .into_iter() + .collect() + }); + status.add_column(StatusColumn::last_update()); + } + Some(StatusOutput::Wide) => { + status.new_column("BUILD ID", |crd| { + crd.labels() + .get("testsys/build-id") + .cloned() + .into_iter() + .collect() + }); + status.add_column(StatusColumn::last_update()); + } + }; + + let (width, _) = terminal_size::terminal_size() + .map(|(w, h)| (w.0 as usize, h.0)) + .unwrap_or((80, 0)); + debug!("Window width '{width}'"); + println!("{status:width$}"); + + Ok(()) + } +} + +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "kebab-case")] +enum StatusOutput { + /// Output the status in json + Json, + /// Show minimal columns in the status table + Narrow, + /// Show all columns in the status table + Wide, +} + +derive_fromstr_from_deserialize!(StatusOutput); diff --git a/testsys-launcher/testsys/src/uninstall.rs b/testsys-launcher/testsys/src/uninstall.rs new file mode 100644 index 00000000..5a55f0fc --- /dev/null +++ b/testsys-launcher/testsys/src/uninstall.rs @@ -0,0 +1,21 @@ +use crate::error::Result; +use clap::Parser; +use log::{info, trace}; +use testsys_model::test_manager::TestManager; + +/// The uninstall subcommand is responsible for removing all of the components for testsys in +/// a k8s cluster. This is completed by removing the `testsys-bottlerocket-aws` namespace. +#[derive(Debug, Parser)] +pub(crate) struct Uninstall {} + +impl Uninstall { + pub(crate) async fn run(self, client: TestManager) -> Result<()> { + trace!("Uninstalling testsys"); + + client.uninstall().await?; + + info!("testsys components were successfully uninstalled."); + + Ok(()) + } +} diff --git a/testsys-launcher/testsys/src/vmware_k8s.rs b/testsys-launcher/testsys/src/vmware_k8s.rs new file mode 100644 index 00000000..50143ca4 --- /dev/null +++ b/testsys-launcher/testsys/src/vmware_k8s.rs @@ -0,0 +1,300 @@ +use crate::crds::{ + BottlerocketInput, ClusterInput, CrdCreator, CrdInput, CreateCrdOutput, MigrationInput, + TestInput, +}; +use crate::error::{self, Result}; +use crate::migration::migration_crd; +use crate::sonobuoy::{sonobuoy_crd, workload_crd}; +use bottlerocket_types::agent_config::{ + CreationPolicy, CustomUserData, K8sVersion, VSphereK8sClusterConfig, VSphereK8sClusterInfo, + VSphereVmConfig, +}; +use maplit::btreemap; +use pubsys_config::vmware::Datacenter; +use snafu::{OptionExt, ResultExt}; +use std::collections::BTreeMap; +use std::iter::repeat_with; +use std::str::FromStr; +use testsys_model::{Crd, DestructionPolicy, SecretName}; + +/// A `CrdCreator` responsible for creating crd related to `vmware-k8s` variants. +pub(crate) struct VmwareK8sCreator { + pub(crate) region: String, + pub(crate) datacenter: Datacenter, + pub(crate) creds: Option<(String, SecretName)>, + pub(crate) ova_name: String, + pub(crate) encoded_mgmt_cluster_kubeconfig: String, +} + +#[async_trait::async_trait] +impl CrdCreator for VmwareK8sCreator { + /// Use the provided OVA name for the image id. + async fn image_id(&self, _: &CrdInput) -> Result { + Ok(self.ova_name.to_string()) + } + + /// Use standard naming conventions to predict the starting OVA. + async fn starting_image_id(&self, crd_input: &CrdInput) -> Result { + Ok(format!( + "bottlerocket-{}-{}-{}.ova", + crd_input.variant, + crd_input.arch, + crd_input + .starting_version + .as_ref() + .context(error::InvalidSnafu { + what: "The starting version must be provided for migration testing" + })? + )) + } + + /// Creates a vSphere K8s cluster CRD with the `cluster_name` in `cluster_input`. + async fn cluster_crd<'a>(&self, cluster_input: ClusterInput<'a>) -> Result { + let control_plane_endpoint = cluster_input + .crd_input + .config + .control_plane_endpoint + .as_ref() + .context(error::InvalidSnafu { + what: "The control plane endpoint is required for VMware cluster creation.", + })?; + let labels = cluster_input.crd_input.labels(btreemap! { + "testsys/type".to_string() => "cluster".to_string(), + "testsys/cluster".to_string() => cluster_input.cluster_name.to_string(), + "testsys/controlPlaneEndpoint".to_string() => control_plane_endpoint.to_string(), + }); + + // Check if the cluster already has a CRD + if let Some(cluster_crd) = cluster_input + .crd_input + .existing_crds( + &labels, + &[ + "testsys/cluster", + "testsys/type", + "testsys/controlPlaneEndpoint", + ], + ) + .await? + .pop() + { + return Ok(CreateCrdOutput::ExistingCrd(cluster_crd)); + } + + // Check if an existing cluster is using this endpoint + let existing_clusters = cluster_input + .crd_input + .existing_crds(&labels, &["testsys/type", "testsys/controlPlaneEndpoint"]) + .await?; + + let cluster_version = + K8sVersion::from_str(cluster_input.crd_input.variant.version().context( + error::MissingSnafu { + item: "K8s version".to_string(), + what: "aws-k8s variant".to_string(), + }, + )?) + .map_err(|_| error::Error::K8sVersion { + version: cluster_input.crd_input.variant.to_string(), + })?; + + let vsphere_k8s_crd = VSphereK8sClusterConfig::builder() + .name(cluster_input.cluster_name) + .set_labels(Some(labels)) + .control_plane_endpoint_ip(control_plane_endpoint) + .creation_policy(CreationPolicy::IfNotExists) + .version(cluster_version) + .ova_name(self.image_id(cluster_input.crd_input).await?) + .tuf_repo( + cluster_input + .crd_input + .tuf_repo_config() + .context(error::InvalidSnafu { + what: "TUF repo information is required for VMware cluster creation.", + })?, + ) + .vcenter_host_url(&self.datacenter.vsphere_url) + .vcenter_datacenter(&self.datacenter.datacenter) + .vcenter_datastore(&self.datacenter.datastore) + .vcenter_network(&self.datacenter.network) + .vcenter_resource_pool(&self.datacenter.resource_pool) + .vcenter_workload_folder(&self.datacenter.folder) + .mgmt_cluster_kubeconfig_base64(&self.encoded_mgmt_cluster_kubeconfig) + .assume_role(cluster_input.crd_input.config.agent_role.clone()) + .eks_a_release_manifest_url( + cluster_input + .crd_input + .config + .dev + .eks_a_release_manifest_url + .clone(), + ) + .set_conflicts_with(Some(existing_clusters)) + .destruction_policy( + cluster_input + .crd_input + .config + .dev + .cluster_destruction_policy + .to_owned() + .unwrap_or(DestructionPolicy::OnTestSuccess), + ) + .image( + cluster_input + .crd_input + .images + .vsphere_k8s_cluster_resource_agent_image + .as_ref() + .expect( + "The default vSphere K8s cluster resource provider image URI is missing.", + ), + ) + .set_image_pull_secret( + cluster_input + .crd_input + .images + .testsys_agent_pull_secret + .to_owned(), + ) + .set_secrets(Some( + cluster_input + .crd_input + .config + .secrets + .clone() + .into_iter() + .chain(self.creds.clone()) + .collect(), + )) + .privileged(true) + .build(cluster_input.cluster_name) + .context(error::BuildSnafu { + what: "vSphere K8s cluster CRD", + })?; + Ok(CreateCrdOutput::NewCrd(Box::new(Crd::Resource( + vsphere_k8s_crd, + )))) + } + + /// Create a vSphere VM provider CRD to launch Bottlerocket VMs on the cluster created by + /// `cluster_crd`. + async fn bottlerocket_crd<'a>( + &self, + bottlerocket_input: BottlerocketInput<'a>, + ) -> Result { + let cluster_name = bottlerocket_input + .cluster_crd_name + .as_ref() + .expect("A vSphere K8s cluster provider is required"); + let labels = bottlerocket_input.crd_input.labels(btreemap! { + "testsys/type".to_string() => "vms".to_string(), + "testsys/cluster".to_string() => cluster_name.to_string(), + }); + + // Check if other VMs are using this cluster + let existing_clusters = bottlerocket_input + .crd_input + .existing_crds(&labels, &["testsys/type", "testsys/cluster"]) + .await?; + + let suffix: String = repeat_with(fastrand::lowercase).take(4).collect(); + let vsphere_vm_crd = VSphereVmConfig::builder() + .ova_name(bottlerocket_input.image_id) + .tuf_repo(bottlerocket_input.crd_input.tuf_repo_config().context( + error::InvalidSnafu { + what: "TUF repo information is required for Bottlerocket vSphere VM creation.", + }, + )?) + .vcenter_host_url(&self.datacenter.vsphere_url) + .vcenter_datacenter(&self.datacenter.datacenter) + .vcenter_datastore(&self.datacenter.datastore) + .vcenter_network(&self.datacenter.network) + .vcenter_resource_pool(&self.datacenter.resource_pool) + .vcenter_workload_folder(&self.datacenter.folder) + .cluster(VSphereK8sClusterInfo { + name: format!("${{{cluster_name}.clusterName}}"), + control_plane_endpoint_ip: format!("${{{cluster_name}.endpoint}}"), + kubeconfig_base64: format!("${{{cluster_name}.encodedKubeconfig}}"), + }) + .custom_user_data( + bottlerocket_input + .crd_input + .encoded_userdata()? + .map(|encoded_userdata| CustomUserData::Merge { encoded_userdata }), + ) + .assume_role(bottlerocket_input.crd_input.config.agent_role.clone()) + .set_labels(Some(labels)) + .set_conflicts_with(Some(existing_clusters)) + .destruction_policy( + bottlerocket_input + .crd_input + .config + .dev + .bottlerocket_destruction_policy + .to_owned() + .unwrap_or(DestructionPolicy::OnTestSuccess), + ) + .image( + bottlerocket_input + .crd_input + .images + .vsphere_vm_resource_agent_image + .as_ref() + .expect("The default vSphere VM resource provider image URI is missing."), + ) + .set_image_pull_secret( + bottlerocket_input + .crd_input + .images + .testsys_agent_pull_secret + .to_owned(), + ) + .set_secrets(Some( + bottlerocket_input + .crd_input + .config + .secrets + .clone() + .into_iter() + .chain(self.creds.clone()) + .collect(), + )) + .depends_on(cluster_name) + .build(format!("{cluster_name}-vms-{suffix}")) + .context(error::BuildSnafu { + what: "vSphere VM CRD", + })?; + Ok(CreateCrdOutput::NewCrd(Box::new(Crd::Resource( + vsphere_vm_crd, + )))) + } + + async fn migration_crd<'a>( + &self, + migration_input: MigrationInput<'a>, + ) -> Result { + Ok(CreateCrdOutput::NewCrd(Box::new(Crd::Test(migration_crd( + migration_input, + // Let the migration test's SSM RunDocuments and RunCommand invocations happen in 'us-west-2' + // FIXME: Do we need to allow this to be configurable? + Some("us-west-2".to_string()), + "instanceIds", + )?)))) + } + + async fn test_crd<'a>(&self, test_input: TestInput<'a>) -> Result { + Ok(CreateCrdOutput::NewCrd(Box::new(Crd::Test(sonobuoy_crd( + test_input, + )?)))) + } + + async fn workload_crd<'a>(&self, test_input: TestInput<'a>) -> Result { + Ok(CreateCrdOutput::NewCrd(Box::new(Crd::Test(workload_crd( + test_input, + )?)))) + } + + fn additional_fields(&self, _test_type: &str) -> BTreeMap { + btreemap! {"region".to_string() => self.region.clone()} + } +}