From 96d62ee21599f6dadf56dcb69fd9b79a4edabfcf Mon Sep 17 00:00:00 2001 From: Przemyslaw Olszewski Date: Sun, 30 Aug 2026 20:35:02 +0200 Subject: [PATCH] fix: remove legacy run projection --- diff --git a/Cargo.lock b/Cargo.lock --- a/Cargo.lock +++ b/Cargo.lock @@ -1,3367 +1,3366 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aho-corasick" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" -dependencies = [ - "memchr", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" - -[[package]] -name = "arraydeque" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-trait" -version = "0.1.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "atoi" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" -dependencies = [ - "num-traits", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "aws-lc-rs" -version = "1.17.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", - "pkg-config", -] - -[[package]] -name = "axum" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" -dependencies = [ - "axum-core", - "bytes", - "form_urlencoded", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "serde_core", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "sync_wrapper", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bitflags" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" -dependencies = [ - "serde_core", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block-buffer" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "bstr" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" -dependencies = [ - "memchr", - "regex-automata", - "serde_core", -] - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" - -[[package]] -name = "cc" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" - -[[package]] -name = "chacha20" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "rand_core", -] - -[[package]] -name = "clap" -version = "4.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - -[[package]] -name = "cmov" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "const-oid" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" - -[[package]] -name = "crc32fast" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "crypto-common" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "ctutils" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" -dependencies = [ - "cmov", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "crypto-common 0.1.7", - "subtle", -] - -[[package]] -name = "digest" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" -dependencies = [ - "block-buffer 0.12.1", - "const-oid", - "crypto-common 0.2.2", - "ctutils", -] - -[[package]] -name = "displaydoc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "either" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" -dependencies = [ - "serde", -] - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "etcetera" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" -dependencies = [ - "cfg-if", - "windows-sys 0.61.2", -] - -[[package]] -name = "event-listener" -version = "5.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" -dependencies = [ - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "fastrand" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" - -[[package]] -name = "filetime" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" -dependencies = [ - "cfg-if", - "libc", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "flume" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" -dependencies = [ - "futures-core", - "futures-sink", - "spin", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[package]] -name = "futures" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" - -[[package]] -name = "futures-executor" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-intrusive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" -dependencies = [ - "futures-core", - "lock_api", - "parking_lot", -] - -[[package]] -name = "futures-io" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" - -[[package]] -name = "futures-macro" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "futures-sink" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" - -[[package]] -name = "futures-task" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" - -[[package]] -name = "futures-util" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "rand_core", - "wasm-bindgen", -] - -[[package]] -name = "gix-validate" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dae8780f63ed8a803b8bdabbd7aa5f5c5d74592c8b50eed875c1bb4f6545a6a" -dependencies = [ - "bstr", -] - -[[package]] -name = "h2" -version = "0.4.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -dependencies = [ - "foldhash 0.2.0", -] - -[[package]] -name = "hashlink" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" -dependencies = [ - "hashbrown 0.16.1", -] - -[[package]] -name = "hashlink" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" -dependencies = [ - "hashbrown 0.17.1", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hkdf" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" -dependencies = [ - "hmac 0.13.0", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "hmac" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" -dependencies = [ - "digest 0.11.3", -] - -[[package]] -name = "http" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hybrid-array" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" -dependencies = [ - "typenum", -] - -[[package]] -name = "hyper" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-timeout" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" -dependencies = [ - "hyper", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "system-configuration", - "tokio", - "tower-service", - "tracing", - "windows-registry", -] - -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", -] - -[[package]] -name = "ipnet" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jni" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" -dependencies = [ - "cfg-if", - "combine", - "jni-macros", - "jni-sys", - "log", - "simd_cesu8", - "thiserror", - "walkdir", - "windows-link", -] - -[[package]] -name = "jni-macros" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn 2.0.119", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.119", -] - -[[package]] -name = "jobserver" -version = "0.1.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" -dependencies = [ - "getrandom 0.4.3", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "libc" -version = "0.2.189" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" - -[[package]] -name = "libsqlite3-sys" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" -dependencies = [ - "pkg-config", - "vcpkg", -] - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "matchit" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" - -[[package]] -name = "md-5" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" -dependencies = [ - "cfg-if", - "digest 0.11.3", -] - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "multimap" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "ordered-float" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" -dependencies = [ - "num-traits", -] - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "petgraph" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" -dependencies = [ - "fixedbitset", - "hashbrown 0.15.5", - "indexmap", -] - -[[package]] -name = "pin-project" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.119", -] - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "prost" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-build" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" -dependencies = [ - "heck", - "itertools", - "log", - "multimap", - "petgraph", - "prettyplease", - "prost", - "prost-types", - "pulldown-cmark", - "pulldown-cmark-to-cmark", - "regex", - "syn 2.0.119", - "tempfile", -] - -[[package]] -name = "prost-derive" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" -dependencies = [ - "anyhow", - "itertools", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "prost-types" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" -dependencies = [ - "prost", -] - -[[package]] -name = "protoc-bin-vendored" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1c381df33c98266b5f08186583660090a4ffa0889e76c7e9a5e175f645a67fa" -dependencies = [ - "protoc-bin-vendored-linux-aarch_64", - "protoc-bin-vendored-linux-ppcle_64", - "protoc-bin-vendored-linux-s390_64", - "protoc-bin-vendored-linux-x86_32", - "protoc-bin-vendored-linux-x86_64", - "protoc-bin-vendored-macos-aarch_64", - "protoc-bin-vendored-macos-x86_64", - "protoc-bin-vendored-win32", -] - -[[package]] -name = "protoc-bin-vendored-linux-aarch_64" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c350df4d49b5b9e3ca79f7e646fde2377b199e13cfa87320308397e1f37e1a4c" - -[[package]] -name = "protoc-bin-vendored-linux-ppcle_64" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55a63e6c7244f19b5c6393f025017eb5d793fd5467823a099740a7a4222440c" - -[[package]] -name = "protoc-bin-vendored-linux-s390_64" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dba5565db4288e935d5330a07c264a4ee8e4a5b4a4e6f4e83fad824cc32f3b0" - -[[package]] -name = "protoc-bin-vendored-linux-x86_32" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8854774b24ee28b7868cd71dccaae8e02a2365e67a4a87a6cd11ee6cdbdf9cf5" - -[[package]] -name = "protoc-bin-vendored-linux-x86_64" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b38b07546580df720fa464ce124c4b03630a6fb83e05c336fea2a241df7e5d78" - -[[package]] -name = "protoc-bin-vendored-macos-aarch_64" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89278a9926ce312e51f1d999fee8825d324d603213344a9a706daa009f1d8092" - -[[package]] -name = "protoc-bin-vendored-macos-x86_64" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81745feda7ccfb9471d7a4de888f0652e806d5795b61480605d4943176299756" - -[[package]] -name = "protoc-bin-vendored-win32" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3" - -[[package]] -name = "pulldown-cmark" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" -dependencies = [ - "bitflags", - "memchr", - "unicase", -] - -[[package]] -name = "pulldown-cmark-to-cmark" -version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50793def1b900256624a709439404384204a5dc3a6ec580281bfaac35e882e90" -dependencies = [ - "pulldown-cmark", -] - -[[package]] -name = "quinn" -version = "0.11.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.4.3", - "lru-slab", - "rand", - "rand_pcg", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.61.2", -] - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" -dependencies = [ - "chacha20", - "getrandom 0.4.3", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - -[[package]] -name = "rand_pcg" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" -dependencies = [ - "rand_core", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "reqwest" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "mime", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "serde", - "serde_json", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustc-hash" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" -dependencies = [ - "aws-lc-rs", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pki-types" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" -dependencies = [ - "core-foundation 0.10.1", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - -[[package]] -name = "rustls-webpki" -version = "0.103.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -dependencies = [ - "aws-lc-rs", - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "saphyr" -version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acd6cfc4803660d26a3fb5bd8f5e47bc0eea5229f4d32f7cb4ee21a733e6961" -dependencies = [ - "hashlink 0.12.1", - "ordered-float", - "saphyr-parser", - "thiserror", -] - -[[package]] -name = "saphyr-parser" -version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebfd783fcf1b3f6bafd557be0e1427ec54f826f513c3cdd749f9844484df2a13" -dependencies = [ - "arraydeque", - "thiserror", -] - -[[package]] -name = "schannel" -version = "0.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "security-framework" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_path_to_error" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" -dependencies = [ - "itoa", - "serde", - "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha1" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.3", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.3", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "simd-adler32" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" - -[[package]] -name = "simd_cesu8" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" -dependencies = [ - "rustc_version", - "simdutf8", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" -dependencies = [ - "serde", -] - -[[package]] -name = "socket2" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "spin" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" -dependencies = [ - "lock_api", -] - -[[package]] -name = "sqlx" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" -dependencies = [ - "sqlx-core", - "sqlx-macros", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", -] - -[[package]] -name = "sqlx-core" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" -dependencies = [ - "base64", - "bytes", - "cfg-if", - "crc", - "crossbeam-queue", - "either", - "event-listener", - "futures-core", - "futures-intrusive", - "futures-io", - "futures-util", - "hashbrown 0.16.1", - "hashlink 0.11.1", - "indexmap", - "log", - "memchr", - "percent-encoding", - "rustls", - "serde", - "serde_json", - "sha2 0.10.9", - "smallvec", - "thiserror", - "tokio", - "tokio-stream", - "tracing", - "url", - "uuid", - "webpki-roots", -] - -[[package]] -name = "sqlx-macros" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" -dependencies = [ - "proc-macro2", - "quote", - "sqlx-core", - "sqlx-macros-core", - "syn 2.0.119", -] - -[[package]] -name = "sqlx-macros-core" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" -dependencies = [ - "cfg-if", - "dotenvy", - "either", - "heck", - "hex", - "proc-macro2", - "quote", - "serde", - "serde_json", - "sha2 0.10.9", - "sqlx-core", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", - "syn 2.0.119", - "thiserror", - "tokio", - "url", -] - -[[package]] -name = "sqlx-mysql" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" -dependencies = [ - "bitflags", - "byteorder", - "bytes", - "crc", - "digest 0.11.3", - "dotenvy", - "either", - "futures-core", - "futures-util", - "generic-array", - "log", - "percent-encoding", - "serde", - "sha1", - "sha2 0.11.0", - "sqlx-core", - "thiserror", - "tracing", - "uuid", -] - -[[package]] -name = "sqlx-postgres" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" -dependencies = [ - "atoi", - "base64", - "bitflags", - "byteorder", - "crc", - "dotenvy", - "etcetera", - "futures-channel", - "futures-core", - "futures-util", - "hex", - "hkdf", - "hmac 0.13.0", - "itoa", - "log", - "md-5", - "memchr", - "rand", - "serde", - "serde_json", - "sha2 0.11.0", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror", - "tracing", - "uuid", - "whoami", -] - -[[package]] -name = "sqlx-sqlite" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" -dependencies = [ - "atoi", - "flume", - "form_urlencoded", - "futures-channel", - "futures-core", - "futures-executor", - "futures-intrusive", - "futures-util", - "libsqlite3-sys", - "log", - "percent-encoding", - "serde", - "sqlx-core", - "thiserror", - "tracing", - "url", - "uuid", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "syncode-control" -version = "0.5.0" -dependencies = [ - "axum", - "base64", - "clap", - "flate2", - "futures", - "hex", - "hmac 0.13.0", - "reqwest", - "serde", - "serde_json", - "sha2 0.11.0", - "sqlx", - "syncode-control-node", - "syncode-control-nodes", - "syncode-control-runs", - "syncode-control-store", - "syncode-repository-api-grpc", - "syncode-workflow", - "syncode-workflow-github-actions", - "tar", - "tempfile", - "thiserror", - "tokio", - "tokio-stream", - "tonic", - "tower", - "url", - "uuid", -] - -[[package]] -name = "syncode-control-node" -version = "0.5.0" -dependencies = [ - "base64", - "hmac 0.12.1", - "prost", - "prost-build", - "protoc-bin-vendored", - "reqwest", - "serde", - "serde_json", - "sha2 0.10.9", - "syncode-control-nodes", - "syncode-control-runs", - "thiserror", - "tokio", - "tokio-stream", - "tonic", - "tonic-prost", - "tonic-prost-build", - "url", - "uuid", -] - -[[package]] -name = "syncode-control-nodes" -version = "0.5.0" -dependencies = [ - "async-lock", - "serde", - "syncode-control-runs", - "thiserror", - "uuid", -] - -[[package]] -name = "syncode-control-runs" -version = "0.5.0" -dependencies = [ - "async-lock", - "event-listener", - "serde", - "serde_json", - "thiserror", - "uuid", -] - -[[package]] -name = "syncode-control-store" -version = "0.5.0" -dependencies = [ - "serde", - "serde_json", - "sqlx", - "syncode-control-nodes", - "syncode-control-runs", - "thiserror", - "uuid", -] - -[[package]] -name = "syncode-repository-api-grpc" -version = "0.5.0" -source = "git+https://syncode.sh/syncode/repo.git?rev=d293b2a4b948be059f61283ffc3ab2ec220a64e4#d293b2a4b948be059f61283ffc3ab2ec220a64e4" -dependencies = [ - "prost", - "prost-build", - "protoc-bin-vendored", - "syncode-repository-application", - "syncode-repository-model", - "tokio", - "tokio-stream", - "tonic", - "tonic-prost", - "tonic-prost-build", - "uuid", -] - -[[package]] -name = "syncode-repository-application" -version = "0.5.0" -source = "git+https://syncode.sh/syncode/repo.git?rev=d293b2a4b948be059f61283ffc3ab2ec220a64e4#d293b2a4b948be059f61283ffc3ab2ec220a64e4" -dependencies = [ - "async-trait", - "serde_json", - "syncode-repository-model", - "thiserror", - "uuid", -] - -[[package]] -name = "syncode-repository-model" -version = "0.5.0" -source = "git+https://syncode.sh/syncode/repo.git?rev=d293b2a4b948be059f61283ffc3ab2ec220a64e4#d293b2a4b948be059f61283ffc3ab2ec220a64e4" -dependencies = [ - "bstr", - "gix-validate", - "hex", - "thiserror", - "uuid", -] - -[[package]] -name = "syncode-workflow" -version = "0.4.0" -source = "git+https://syncode.sh/syncode/workflow.git?rev=14de090ff1961286a947d34f9d81c091081233f1#14de090ff1961286a947d34f9d81c091081233f1" -dependencies = [ - "serde", - "thiserror", - "url", -] - -[[package]] -name = "syncode-workflow-github-actions" -version = "0.4.0" -source = "git+https://syncode.sh/syncode/workflow.git?rev=14de090ff1961286a947d34f9d81c091081233f1#14de090ff1961286a947d34f9d81c091081233f1" -dependencies = [ - "saphyr", - "serde", - "serde_json", - "syncode-workflow", - "thiserror", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -dependencies = [ - "bitflags", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tar" -version = "0.4.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" -dependencies = [ - "filetime", - "libc", - "xattr", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.3", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "thiserror" -version = "2.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "libc", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tonic" -version = "0.14.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" -dependencies = [ - "async-trait", - "axum", - "base64", - "bytes", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-timeout", - "hyper-util", - "percent-encoding", - "pin-project", - "socket2", - "sync_wrapper", - "tokio", - "tokio-stream", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tonic-build" -version = "0.14.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322" -dependencies = [ - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "tonic-prost" -version = "0.14.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" -dependencies = [ - "bytes", - "prost", - "tonic", -] - -[[package]] -name = "tonic-prost-build" -version = "0.14.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27" -dependencies = [ - "prettyplease", - "proc-macro2", - "prost-build", - "prost-types", - "quote", - "syn 2.0.119", - "tempfile", - "tonic-build", -] - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "indexmap", - "pin-project-lite", - "slab", - "sync_wrapper", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", - "url", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typenum" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-properties" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" -dependencies = [ - "getrandom 0.4.3", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasm-bindgen" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.119", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "web-sys" -version = "0.3.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" -dependencies = [ - "js-sys", - "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-root-certs" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "webpki-roots" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "whoami" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[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_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix", -] - -[[package]] -name = "yoke" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] - -[[package]] -name = "zerofrom" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" - -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core", +] + +[[package]] +name = "clap" +version = "4.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crc32fast" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core", + "wasm-bindgen", +] + +[[package]] +name = "gix-validate" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dae8780f63ed8a803b8bdabbd7aa5f5c5d74592c8b50eed875c1bb4f6545a6a" +dependencies = [ + "bstr", +] + +[[package]] +name = "h2" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "hashlink" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac 0.13.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "ordered-float" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "pulldown-cmark", + "pulldown-cmark-to-cmark", + "regex", + "syn 2.0.119", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "prost-types" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" +dependencies = [ + "prost", +] + +[[package]] +name = "protoc-bin-vendored" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c381df33c98266b5f08186583660090a4ffa0889e76c7e9a5e175f645a67fa" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-s390_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-aarch_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c350df4d49b5b9e3ca79f7e646fde2377b199e13cfa87320308397e1f37e1a4c" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55a63e6c7244f19b5c6393f025017eb5d793fd5467823a099740a7a4222440c" + +[[package]] +name = "protoc-bin-vendored-linux-s390_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dba5565db4288e935d5330a07c264a4ee8e4a5b4a4e6f4e83fad824cc32f3b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8854774b24ee28b7868cd71dccaae8e02a2365e67a4a87a6cd11ee6cdbdf9cf5" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b38b07546580df720fa464ce124c4b03630a6fb83e05c336fea2a241df7e5d78" + +[[package]] +name = "protoc-bin-vendored-macos-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89278a9926ce312e51f1d999fee8825d324d603213344a9a706daa009f1d8092" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81745feda7ccfb9471d7a4de888f0652e806d5795b61480605d4943176299756" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3" + +[[package]] +name = "pulldown-cmark" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "pulldown-cmark-to-cmark" +version = "22.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50793def1b900256624a709439404384204a5dc3a6ec580281bfaac35e882e90" +dependencies = [ + "pulldown-cmark", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "aws-lc-rs", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "saphyr" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acd6cfc4803660d26a3fb5bd8f5e47bc0eea5229f4d32f7cb4ee21a733e6961" +dependencies = [ + "hashlink 0.12.1", + "ordered-float", + "saphyr-parser", + "thiserror", +] + +[[package]] +name = "saphyr-parser" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebfd783fcf1b3f6bafd557be0e1427ec54f826f513c3cdd749f9844484df2a13" +dependencies = [ + "arraydeque", + "thiserror", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlx" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" +dependencies = [ + "base64", + "bytes", + "cfg-if", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.16.1", + "hashlink 0.11.1", + "indexmap", + "log", + "memchr", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots", +] + +[[package]] +name = "sqlx-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.119", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" +dependencies = [ + "cfg-if", + "dotenvy", + "either", + "heck", + "hex", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.119", + "thiserror", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" +dependencies = [ + "bitflags", + "byteorder", + "bytes", + "crc", + "digest 0.11.3", + "dotenvy", + "either", + "futures-core", + "futures-util", + "generic-array", + "log", + "percent-encoding", + "serde", + "sha1", + "sha2 0.11.0", + "sqlx-core", + "thiserror", + "tracing", + "uuid", +] + +[[package]] +name = "sqlx-postgres" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac 0.13.0", + "itoa", + "log", + "md-5", + "memchr", + "rand", + "serde", + "serde_json", + "sha2 0.11.0", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" +dependencies = [ + "atoi", + "flume", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "thiserror", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "syncode-control" +version = "0.5.0" +dependencies = [ + "axum", + "base64", + "clap", + "flate2", + "futures", + "hex", + "hmac 0.13.0", + "reqwest", + "serde", + "serde_json", + "sha2 0.11.0", + "sqlx", + "syncode-control-node", + "syncode-control-nodes", + "syncode-control-runs", + "syncode-control-store", + "syncode-repository-api-grpc", + "syncode-workflow", + "syncode-workflow-github-actions", + "tar", + "tempfile", + "thiserror", + "tokio", + "tokio-stream", + "tonic", + "tower", + "url", + "uuid", +] + +[[package]] +name = "syncode-control-node" +version = "0.5.0" +dependencies = [ + "base64", + "hmac 0.12.1", + "prost", + "prost-build", + "protoc-bin-vendored", + "serde", + "serde_json", + "sha2 0.10.9", + "syncode-control-nodes", + "syncode-control-runs", + "thiserror", + "tokio", + "tokio-stream", + "tonic", + "tonic-prost", + "tonic-prost-build", + "url", + "uuid", +] + +[[package]] +name = "syncode-control-nodes" +version = "0.5.0" +dependencies = [ + "async-lock", + "serde", + "syncode-control-runs", + "thiserror", + "uuid", +] + +[[package]] +name = "syncode-control-runs" +version = "0.5.0" +dependencies = [ + "async-lock", + "event-listener", + "serde", + "serde_json", + "thiserror", + "uuid", +] + +[[package]] +name = "syncode-control-store" +version = "0.5.0" +dependencies = [ + "serde", + "serde_json", + "sqlx", + "syncode-control-nodes", + "syncode-control-runs", + "thiserror", + "uuid", +] + +[[package]] +name = "syncode-repository-api-grpc" +version = "0.5.0" +source = "git+https://syncode.sh/syncode/repo.git?rev=d293b2a4b948be059f61283ffc3ab2ec220a64e4#d293b2a4b948be059f61283ffc3ab2ec220a64e4" +dependencies = [ + "prost", + "prost-build", + "protoc-bin-vendored", + "syncode-repository-application", + "syncode-repository-model", + "tokio", + "tokio-stream", + "tonic", + "tonic-prost", + "tonic-prost-build", + "uuid", +] + +[[package]] +name = "syncode-repository-application" +version = "0.5.0" +source = "git+https://syncode.sh/syncode/repo.git?rev=d293b2a4b948be059f61283ffc3ab2ec220a64e4#d293b2a4b948be059f61283ffc3ab2ec220a64e4" +dependencies = [ + "async-trait", + "serde_json", + "syncode-repository-model", + "thiserror", + "uuid", +] + +[[package]] +name = "syncode-repository-model" +version = "0.5.0" +source = "git+https://syncode.sh/syncode/repo.git?rev=d293b2a4b948be059f61283ffc3ab2ec220a64e4#d293b2a4b948be059f61283ffc3ab2ec220a64e4" +dependencies = [ + "bstr", + "gix-validate", + "hex", + "thiserror", + "uuid", +] + +[[package]] +name = "syncode-workflow" +version = "0.4.0" +source = "git+https://syncode.sh/syncode/workflow.git?rev=14de090ff1961286a947d34f9d81c091081233f1#14de090ff1961286a947d34f9d81c091081233f1" +dependencies = [ + "serde", + "thiserror", + "url", +] + +[[package]] +name = "syncode-workflow-github-actions" +version = "0.4.0" +source = "git+https://syncode.sh/syncode/workflow.git?rev=14de090ff1961286a947d34f9d81c091081233f1#14de090ff1961286a947d34f9d81c091081233f1" +dependencies = [ + "saphyr", + "serde", + "serde_json", + "syncode-workflow", + "thiserror", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tonic" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" +dependencies = [ + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "socket2", + "sync_wrapper", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tonic-prost" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn 2.0.119", + "tempfile", + "tonic-build", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "indexmap", + "pin-project-lite", + "slab", + "sync_wrapper", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "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-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[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_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/README.md b/README.md --- a/README.md +++ b/README.md @@ -1,37 +1,37 @@ -# SynCode Control Plane - -`syncode-control` is the authority over CI runs in SynCode: it triggers runs, -resolves and compiles workflows, owns the run aggregate, keeps the queue, -assigns work to nodes, holds leases, collects state and logs, issues secrets on -demand, enforces quotas, and records the audit trail. - -It replaces the Actions control plane the forge used to run. After 0.3 a node -does not talk to the forge at all: it enrolls here, receives a compiled -execution plan here, and reports here. The forge keeps Git hosting, the secret -store, artifact hosting, the run UI reading a one-way projection, and the event -feed this service triggers runs from. - -## Boundaries - -A workflow is compiled exactly once, in this service, with the shared compiler -from [`syncode/workflow`](https://syncode.sh/syncode/workflow). The assignment -carries a versioned execution plan, not a workflow file: the runner never sees -workflow source and rejects a plan whose schema version it does not support. - -What stays on the node is what cannot be decided before the job runs: fetching -actions, expanding composite actions, and evaluating expressions that depend on -step results, job status, and the environment. - -## Development - -```sh -cargo fmt --check -cargo clippy --workspace --all-targets --all-features -- -D warnings -cargo test --workspace --all-targets --all-features -./scripts/check-architecture.sh -./scripts/check-rust-loc.sh -``` - -## License - -MIT. See [LICENSE](LICENSE). +# SynCode Control Plane + +`syncode-control` is the authority over CI runs in SynCode: it triggers runs, +resolves and compiles workflows, owns the run aggregate, keeps the queue, +assigns work to nodes, holds leases, collects state and logs, issues secrets on +demand, enforces quotas, and records the audit trail. + +It replaces the Actions control plane the forge used to run. After 0.3 a node +does not talk to the forge at all: it enrolls here, receives a compiled +execution plan here, and reports here. The SynCode UI reads runs and logs from +this service. The forge temporarily keeps the action mirror, secret store, and +the event feed this service triggers runs from. + +## Boundaries + +A workflow is compiled exactly once, in this service, with the shared compiler +from [`syncode/workflow`](https://syncode.sh/syncode/workflow). The assignment +carries a versioned execution plan, not a workflow file: the runner never sees +workflow source and rejects a plan whose schema version it does not support. + +What stays on the node is what cannot be decided before the job runs: fetching +actions, expanding composite actions, and evaluating expressions that depend on +step results, job status, and the environment. + +## Development + +```sh +cargo fmt --check +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test --workspace --all-targets --all-features +./scripts/check-architecture.sh +./scripts/check-rust-loc.sh +``` + +## License + +MIT. See [LICENSE](LICENSE). diff --git a/src/lib.rs b/src/lib.rs --- a/src/lib.rs +++ b/src/lib.rs @@ -1,28 +1,27 @@ -pub mod action_delivery; -pub mod action_oci; -pub mod action_repository; -pub mod action_store; -pub mod actions; -pub mod actions_read; -pub mod actions_read_identity; -pub mod admin; -pub mod check_events; -pub mod checks; -pub mod events; -pub mod intake; -pub mod maintenance; -mod native_events; -pub mod projection; -#[path = "forge.rs"] -pub mod repository; -pub mod repository_credentials; -pub mod repository_grpc; -pub mod repository_sources; -pub mod reusable; -mod secret_reference_syntax; -pub mod secret_references; -pub mod secrets; -pub mod sources; -pub mod token; -pub mod trigger; -pub mod webhook; +pub mod action_delivery; +pub mod action_oci; +pub mod action_repository; +pub mod action_store; +pub mod actions; +pub mod actions_read; +pub mod actions_read_identity; +pub mod admin; +pub mod check_events; +pub mod checks; +pub mod events; +pub mod intake; +pub mod maintenance; +mod native_events; +#[path = "forge.rs"] +pub mod repository; +pub mod repository_credentials; +pub mod repository_grpc; +pub mod repository_sources; +pub mod reusable; +mod secret_reference_syntax; +pub mod secret_references; +pub mod secrets; +pub mod sources; +pub mod token; +pub mod trigger; +pub mod webhook; diff --git a/src/main.rs b/src/main.rs --- a/src/main.rs +++ b/src/main.rs @@ -1,392 +1,378 @@ -use std::error::Error; -use std::net::SocketAddr; -use std::path::PathBuf; -use std::sync::Arc; - -use clap::{Parser, Subcommand, ValueEnum}; -use syncode_control::action_delivery::router as action_delivery_router; -use syncode_control::action_oci::PinnedOciResolver; -use syncode_control::action_repository::NativeActionRepository; -use syncode_control::action_store::FileActionStore; -use syncode_control::actions::ActionResolver; -use syncode_control::actions_read::ActionsRead; -use syncode_control::actions_read_identity::IdentityActionsAuthorization; -use syncode_control::admin::{Admin, router as admin_router}; -use syncode_control::check_events::{CheckEventPublisher, CheckEventPublisherConfig}; -use syncode_control::checks::Checks; -use syncode_control::maintenance; -use syncode_control::projection; -use syncode_control::repository::{RepositoryContents, RepositorySecrets}; -use syncode_control::repository_credentials::IdentityRepositoryCredentials; -use syncode_control::repository_grpc::NativeRepositoryContents; -use syncode_control::repository_sources::RepositorySources; -use syncode_control::secrets::RuntimeSecrets; -use syncode_control::token::EnrolmentScope; -use syncode_control::webhook::{Intake, router}; -use syncode_control_node::{ - ArtifactTokenAuthority, CapabilityAuthority, GeneratedActionsReadServer, GeneratedChecksServer, - GeneratedSecretsServer, GeneratedServer, NodeSessionServer, ProjectionClient, -}; -use syncode_control_nodes::{Nodes, Scope}; -use syncode_control_runs::{ - AuditConfiguration, AuditControlMode, AuditEvent, AuditPayload, NodeId, Runs, SchedulerPolicy, -}; -use syncode_control_store::Postgres; -use tokio::net::TcpListener; -use tonic::transport::Server; -use url::Url; - -#[derive(Debug, Parser)] -#[command(name = "syncode-control", version, about)] -struct Arguments { - #[command(subcommand)] - command: Option, - - /// Where nodes open their session. - #[arg(long, default_value = "127.0.0.1:8090")] - listen: SocketAddr, - - /// Where the native repository event feed delivers events. - #[arg(long, default_value = "127.0.0.1:8091")] - listen_events: SocketAddr, - - /// The repository service this control plane reads workflows from. - #[arg(long, env = "SYNCODE_REPOSITORY_SOURCE_URL")] - repository_source: Url, - - /// A token allowed to read repository contents. - #[arg(long, env = "SYNCODE_REPOSITORY_SOURCE_TOKEN", hide_env_values = true)] - repository_source_token: String, - - /// Where native repositories are read over gRPC. - #[arg(long, env = "SYNCODE_REPOSITORY_GRPC_URL")] - repository_grpc: String, - - #[arg(long, env = "SYNCODE_IDENTITY_GRPC_URL")] - identity_grpc: String, - - #[arg(long, env = "SYNCODE_IDENTITY_SHARED_SECRET", hide_env_values = true)] - identity_shared_secret: String, - - #[arg(long, env = "SYNCODE_ACTION_MIRROR_URL")] - action_mirror_url: Url, - - #[arg( - long, - env = "SYNCODE_ACTION_ALLOWLIST", - value_delimiter = ',', - num_args = 1.. - )] - action_allowlist: Vec, - - #[arg( - long, - env = "SYNCODE_ACTION_OCI_REGISTRIES", - value_delimiter = ',', - num_args = 1.. - )] - action_oci_registries: Vec, - - #[arg(long, env = "SYNCODE_ACTION_STORE")] - action_store: PathBuf, - - #[arg(long, env = "SYNCODE_ACTION_ARTIFACT_PUBLIC_URL")] - action_artifact_public_url: Url, - - #[arg(long, env = "SYNCODE_SECRET_SOURCE_URL")] - secret_source: Url, - - #[arg(long, env = "SYNCODE_SECRET_SOURCE_TOKEN", hide_env_values = true)] - secret_source_token: String, - - #[arg(long, env = "SYNCODE_CAPABILITY_SIGNING_KEY", hide_env_values = true)] - capability_signing_key: String, - - #[arg(long, env = "SYNCODE_ARTIFACT_SIGNING_KEY", hide_env_values = true)] - artifact_signing_key: String, - - #[arg(long, env = "SYNCODE_PROJECTION_URL")] - projection_url: Url, - - #[arg(long, env = "SYNCODE_PROJECTION_TOKEN", hide_env_values = true)] - projection_token: String, - - #[arg(long, env = "SYNCODE_PROJECTION_MIN_RUN_NUMBER")] - projection_min_run_number: u64, - - /// The secret used to sign the native event feed. - #[arg(long, env = "SYNCODE_EVENT_FEED_SECRET", hide_env_values = true)] - event_feed_secret: String, - - #[arg(long, env = "SYNCODE_COLLAB_EVENT_URL")] - collaboration_event_url: Url, - - #[arg(long, env = "SYNCODE_COLLAB_EVENT_SECRET", hide_env_values = true)] - collaboration_event_secret: String, - - #[arg(long, env = "SYNCODE_CONTROL_SOURCE_NODE_ID")] - source_node_id: uuid::Uuid, - - #[arg( - long, - env = "SYNCODE_CHECK_EVENT_INTERVAL_SECONDS", - default_value_t = 1 - )] - check_event_interval_seconds: u64, - - #[arg(long, env = "SYNCODE_CHECK_EVENT_BATCH", default_value_t = 100)] - check_event_batch: i64, - - /// Bearer token protecting the operational API. - #[arg(long, env = "SYNCODE_CONTROL_ADMIN_TOKEN", hide_env_values = true)] - admin_token: Option, - - /// Where the run log is kept. - #[arg(long, env = "SYNCODE_DATABASE_URL", hide_env_values = true)] - database: String, - - #[arg(long, default_value_t = 8)] - database_connections: u32, - - #[arg(long, env = "SYNCODE_ORGANIZATION_CONCURRENCY", default_value_t = 100)] - organization_concurrency: u32, - - #[arg(long, env = "SYNCODE_REPOSITORY_CONCURRENCY", default_value_t = 20)] - repository_concurrency: u32, - - #[arg(long, env = "SYNCODE_PRINCIPAL_CONCURRENCY", default_value_t = 20)] - principal_concurrency: u32, - - #[arg( - long, - env = "SYNCODE_ORGANIZATION_QUEUE_QUOTA", - default_value_t = 10_000 - )] - organization_queue_quota: u32, - - #[arg(long, env = "SYNCODE_PRINCIPAL_QUEUE_QUOTA", default_value_t = 1_000)] - principal_queue_quota: u32, - - #[arg(long, env = "SYNCODE_AUDIT_RETENTION_DAYS", default_value_t = 90)] - audit_retention_days: u32, - - /// Whether compiled shadow runs may be assigned to nodes. - #[arg(long, value_enum, default_value_t = Mode::Shadow)] - mode: Mode, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)] -enum Mode { - Shadow, - Active, -} - -#[derive(Debug, Subcommand)] -enum Command { - /// Issue an enrolment token a node can spend for an identity. - /// - /// The token is stored before it is printed, so one that reaches an - /// operator is one the control plane will honour. - IssueToken { - /// How far the token reaches: `instance`, `organisation:` or - /// `repository:/`. - #[arg(long, default_value = "instance")] - scope: String, - }, - /// Revoke a node identity immediately. - RevokeNode { - /// The node UUID printed at enrolment. - node: String, - }, -} - -#[tokio::main] -async fn main() -> Result<(), Box> { - let arguments = Arguments::parse(); - // The log is read before anything is served: a node that reconnects has to - // meet the run it was holding, not a control plane that has forgotten it. - let store = Postgres::connect(&arguments.database, arguments.database_connections).await?; - let policy = SchedulerPolicy::new( - arguments.organization_concurrency, - arguments.repository_concurrency, - arguments.principal_concurrency, - arguments.organization_queue_quota, - arguments.principal_queue_quota, - ); - let runs = Runs::restored_with_policy(store.clone(), policy).await?; - let nodes = Nodes::restored(store.clone()).await?; - - if let Some(command) = arguments.command { - match command { - Command::IssueToken { scope } => { - let scope: Scope = scope.parse::()?.into(); - let token = nodes.issue_token(scope).await?; - println!("{}", token.secret().expose()); - } - Command::RevokeNode { node } => { - nodes.revoke(node.parse::()?).await?; - } - } - return Ok(()); - } - - store - .record_audit(AuditEvent::new("control.configuration", "applied").after( - AuditPayload::Configuration(AuditConfiguration { - mode: match arguments.mode { - Mode::Shadow => AuditControlMode::Shadow, - Mode::Active => AuditControlMode::Active, - }, - organization_concurrency: arguments.organization_concurrency, - repository_concurrency: arguments.repository_concurrency, - principal_concurrency: arguments.principal_concurrency, - organization_queue_quota: arguments.organization_queue_quota, - principal_queue_quota: arguments.principal_queue_quota, - audit_retention_days: arguments.audit_retention_days, - projection_min_run_number: arguments.projection_min_run_number, - }), - )) - .await?; - tokio::spawn(maintenance::sweep(runs.clone(), nodes.clone())); - tokio::spawn(maintenance::retain_audit( - store.clone(), - arguments.audit_retention_days, - )); - - let action_mirror = arguments - .action_mirror_url - .to_string() - .parse::()?; - let action_allowlist = arguments - .action_allowlist - .into_iter() - .map(|repository| { - repository - .to_string() - .parse::() - }) - .collect::, _>>()?; - let action_store = FileActionStore::new(arguments.action_store); - let action_repository = NativeActionRepository::connect( - arguments.repository_grpc.clone(), - arguments.identity_grpc.clone(), - arguments.identity_shared_secret.clone(), - ) - .await?; - let action_resolver = ActionResolver::new( - action_repository, - action_store.clone(), - PinnedOciResolver::new(arguments.action_oci_registries)?, - action_mirror, - action_allowlist, - ); - let native_repository = - NativeRepositoryContents::connect(arguments.repository_grpc.clone()).await?; - let repository_sources = RepositorySources::new( - native_repository, - RepositoryContents::new( - arguments.repository_source, - arguments.repository_source_token, - ), - ); - let intake = Arc::new(Intake::new( - repository_sources, - runs.clone(), - action_resolver, - arguments.event_feed_secret, - )); - let admin_token = arguments - .admin_token - .filter(|token| !token.is_empty()) - .ok_or("SYNCODE_CONTROL_ADMIN_TOKEN is required while serving")?; - let artifact_authority = ArtifactTokenAuthority::new(arguments.artifact_signing_key)?; - let http = router(intake) - .merge(admin_router(Arc::new( - Admin::new(runs.clone(), nodes.clone(), admin_token).with_operations(store.clone()), - ))) - .merge(action_delivery_router( - action_store, - artifact_authority.clone(), - runs.clone(), - )); - let events = TcpListener::bind(arguments.listen_events).await?; - let check_events = CheckEventPublisher::new( - store.clone(), - CheckEventPublisherConfig { - endpoint: arguments.collaboration_event_url.as_str(), - secret: arguments.collaboration_event_secret, - source_node_id: arguments.source_node_id, - interval: std::time::Duration::from_secs(arguments.check_event_interval_seconds), - batch: arguments.check_event_batch, - }, - )?; - - let authority = CapabilityAuthority::new(arguments.capability_signing_key)?; - let projection = ProjectionClient::new( - arguments.projection_url, - arguments.projection_token, - arguments.identity_grpc.clone(), - arguments.identity_shared_secret.clone(), - )?; - let secret_service = RuntimeSecrets::new( - runs.clone(), - nodes.clone(), - RepositorySecrets::new(arguments.secret_source, arguments.secret_source_token), - IdentityRepositoryCredentials::connect( - arguments.identity_grpc.clone(), - arguments.identity_shared_secret.clone(), - ) - .await?, - authority.clone(), - ); - let actions_read = ActionsRead::new( - runs.clone(), - IdentityActionsAuthorization::connect( - arguments.identity_grpc, - arguments.identity_shared_secret, - ) - .await?, - ); - - // Both ends of the control plane run for as long as the other does: without - // events there is nothing to assign, and without sessions there is nobody to - // assign it to. Whichever stops first takes the process down with it. - let node_service = match arguments.mode { - Mode::Shadow => NodeSessionServer::shadow( - runs.clone(), - nodes, - authority, - artifact_authority, - arguments.action_artifact_public_url, - projection.clone(), - ), - Mode::Active => NodeSessionServer::new( - runs.clone(), - nodes, - authority, - artifact_authority, - arguments.action_artifact_public_url, - projection.clone(), - ), - }; - tokio::select! { - served = axum::serve(events, http).into_future() => served?, - served = projection::serve(runs.clone(), projection, arguments.projection_min_run_number) => served?, - published = check_events.run() => published?, - served = Server::builder() - .add_service(GeneratedServer::new(node_service)) - .add_service(GeneratedSecretsServer::new(secret_service)) - .add_service(GeneratedChecksServer::new(Checks::new(runs.clone()))) - .add_service(GeneratedActionsReadServer::new(actions_read)) - .serve_with_shutdown(arguments.listen, shutdown()) => served?, - } - - Ok(()) -} - -async fn shutdown() { - if let Err(error) = tokio::signal::ctrl_c().await { - eprintln!("cannot listen for shutdown, keeping the service running: {error}"); - std::future::pending::<()>().await; - } -} +use std::error::Error; +use std::net::SocketAddr; +use std::path::PathBuf; +use std::sync::Arc; + +use clap::{Parser, Subcommand, ValueEnum}; +use syncode_control::action_delivery::router as action_delivery_router; +use syncode_control::action_oci::PinnedOciResolver; +use syncode_control::action_repository::NativeActionRepository; +use syncode_control::action_store::FileActionStore; +use syncode_control::actions::ActionResolver; +use syncode_control::actions_read::ActionsRead; +use syncode_control::actions_read_identity::IdentityActionsAuthorization; +use syncode_control::admin::{Admin, router as admin_router}; +use syncode_control::check_events::{CheckEventPublisher, CheckEventPublisherConfig}; +use syncode_control::checks::Checks; +use syncode_control::maintenance; +use syncode_control::repository::{RepositoryContents, RepositorySecrets}; +use syncode_control::repository_credentials::IdentityRepositoryCredentials; +use syncode_control::repository_grpc::NativeRepositoryContents; +use syncode_control::repository_sources::RepositorySources; +use syncode_control::secrets::RuntimeSecrets; +use syncode_control::token::EnrolmentScope; +use syncode_control::webhook::{Intake, router}; +use syncode_control_node::{ + ArtifactTokenAuthority, CapabilityAuthority, GeneratedActionsReadServer, GeneratedChecksServer, + GeneratedSecretsServer, GeneratedServer, NodeSessionServer, RepositoryCoordinates, +}; +use syncode_control_nodes::{Nodes, Scope}; +use syncode_control_runs::{ + AuditConfiguration, AuditControlMode, AuditEvent, AuditPayload, NodeId, Runs, SchedulerPolicy, +}; +use syncode_control_store::Postgres; +use tokio::net::TcpListener; +use tonic::transport::Server; +use url::Url; + +#[derive(Debug, Parser)] +#[command(name = "syncode-control", version, about)] +struct Arguments { + #[command(subcommand)] + command: Option, + + /// Where nodes open their session. + #[arg(long, default_value = "127.0.0.1:8090")] + listen: SocketAddr, + + /// Where the native repository event feed delivers events. + #[arg(long, default_value = "127.0.0.1:8091")] + listen_events: SocketAddr, + + /// The repository service this control plane reads workflows from. + #[arg(long, env = "SYNCODE_REPOSITORY_SOURCE_URL")] + repository_source: Url, + + /// A token allowed to read repository contents. + #[arg(long, env = "SYNCODE_REPOSITORY_SOURCE_TOKEN", hide_env_values = true)] + repository_source_token: String, + + /// Where native repositories are read over gRPC. + #[arg(long, env = "SYNCODE_REPOSITORY_GRPC_URL")] + repository_grpc: String, + + #[arg(long, env = "SYNCODE_IDENTITY_GRPC_URL")] + identity_grpc: String, + + #[arg(long, env = "SYNCODE_IDENTITY_SHARED_SECRET", hide_env_values = true)] + identity_shared_secret: String, + + #[arg(long, env = "SYNCODE_ACTION_MIRROR_URL")] + action_mirror_url: Url, + + #[arg( + long, + env = "SYNCODE_ACTION_ALLOWLIST", + value_delimiter = ',', + num_args = 1.. + )] + action_allowlist: Vec, + + #[arg( + long, + env = "SYNCODE_ACTION_OCI_REGISTRIES", + value_delimiter = ',', + num_args = 1.. + )] + action_oci_registries: Vec, + + #[arg(long, env = "SYNCODE_ACTION_STORE")] + action_store: PathBuf, + + #[arg(long, env = "SYNCODE_ACTION_ARTIFACT_PUBLIC_URL")] + action_artifact_public_url: Url, + + #[arg(long, env = "SYNCODE_SECRET_SOURCE_URL")] + secret_source: Url, + + #[arg(long, env = "SYNCODE_SECRET_SOURCE_TOKEN", hide_env_values = true)] + secret_source_token: String, + + #[arg(long, env = "SYNCODE_CAPABILITY_SIGNING_KEY", hide_env_values = true)] + capability_signing_key: String, + + #[arg(long, env = "SYNCODE_ARTIFACT_SIGNING_KEY", hide_env_values = true)] + artifact_signing_key: String, + + /// The secret used to sign the native event feed. + #[arg(long, env = "SYNCODE_EVENT_FEED_SECRET", hide_env_values = true)] + event_feed_secret: String, + + #[arg(long, env = "SYNCODE_COLLAB_EVENT_URL")] + collaboration_event_url: Url, + + #[arg(long, env = "SYNCODE_COLLAB_EVENT_SECRET", hide_env_values = true)] + collaboration_event_secret: String, + + #[arg(long, env = "SYNCODE_CONTROL_SOURCE_NODE_ID")] + source_node_id: uuid::Uuid, + + #[arg( + long, + env = "SYNCODE_CHECK_EVENT_INTERVAL_SECONDS", + default_value_t = 1 + )] + check_event_interval_seconds: u64, + + #[arg(long, env = "SYNCODE_CHECK_EVENT_BATCH", default_value_t = 100)] + check_event_batch: i64, + + /// Bearer token protecting the operational API. + #[arg(long, env = "SYNCODE_CONTROL_ADMIN_TOKEN", hide_env_values = true)] + admin_token: Option, + + /// Where the run log is kept. + #[arg(long, env = "SYNCODE_DATABASE_URL", hide_env_values = true)] + database: String, + + #[arg(long, default_value_t = 8)] + database_connections: u32, + + #[arg(long, env = "SYNCODE_ORGANIZATION_CONCURRENCY", default_value_t = 100)] + organization_concurrency: u32, + + #[arg(long, env = "SYNCODE_REPOSITORY_CONCURRENCY", default_value_t = 20)] + repository_concurrency: u32, + + #[arg(long, env = "SYNCODE_PRINCIPAL_CONCURRENCY", default_value_t = 20)] + principal_concurrency: u32, + + #[arg( + long, + env = "SYNCODE_ORGANIZATION_QUEUE_QUOTA", + default_value_t = 10_000 + )] + organization_queue_quota: u32, + + #[arg(long, env = "SYNCODE_PRINCIPAL_QUEUE_QUOTA", default_value_t = 1_000)] + principal_queue_quota: u32, + + #[arg(long, env = "SYNCODE_AUDIT_RETENTION_DAYS", default_value_t = 90)] + audit_retention_days: u32, + + /// Whether compiled shadow runs may be assigned to nodes. + #[arg(long, value_enum, default_value_t = Mode::Shadow)] + mode: Mode, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)] +enum Mode { + Shadow, + Active, +} + +#[derive(Debug, Subcommand)] +enum Command { + /// Issue an enrolment token a node can spend for an identity. + /// + /// The token is stored before it is printed, so one that reaches an + /// operator is one the control plane will honour. + IssueToken { + /// How far the token reaches: `instance`, `organisation:` or + /// `repository:/`. + #[arg(long, default_value = "instance")] + scope: String, + }, + /// Revoke a node identity immediately. + RevokeNode { + /// The node UUID printed at enrolment. + node: String, + }, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let arguments = Arguments::parse(); + // The log is read before anything is served: a node that reconnects has to + // meet the run it was holding, not a control plane that has forgotten it. + let store = Postgres::connect(&arguments.database, arguments.database_connections).await?; + let policy = SchedulerPolicy::new( + arguments.organization_concurrency, + arguments.repository_concurrency, + arguments.principal_concurrency, + arguments.organization_queue_quota, + arguments.principal_queue_quota, + ); + let runs = Runs::restored_with_policy(store.clone(), policy).await?; + let nodes = Nodes::restored(store.clone()).await?; + + if let Some(command) = arguments.command { + match command { + Command::IssueToken { scope } => { + let scope: Scope = scope.parse::()?.into(); + let token = nodes.issue_token(scope).await?; + println!("{}", token.secret().expose()); + } + Command::RevokeNode { node } => { + nodes.revoke(node.parse::()?).await?; + } + } + return Ok(()); + } + + store + .record_audit(AuditEvent::new("control.configuration", "applied").after( + AuditPayload::Configuration(AuditConfiguration { + mode: match arguments.mode { + Mode::Shadow => AuditControlMode::Shadow, + Mode::Active => AuditControlMode::Active, + }, + organization_concurrency: arguments.organization_concurrency, + repository_concurrency: arguments.repository_concurrency, + principal_concurrency: arguments.principal_concurrency, + organization_queue_quota: arguments.organization_queue_quota, + principal_queue_quota: arguments.principal_queue_quota, + audit_retention_days: arguments.audit_retention_days, + }), + )) + .await?; + tokio::spawn(maintenance::sweep(runs.clone(), nodes.clone())); + tokio::spawn(maintenance::retain_audit( + store.clone(), + arguments.audit_retention_days, + )); + + let action_mirror = arguments + .action_mirror_url + .to_string() + .parse::()?; + let action_allowlist = arguments + .action_allowlist + .into_iter() + .map(|repository| { + repository + .to_string() + .parse::() + }) + .collect::, _>>()?; + let action_store = FileActionStore::new(arguments.action_store); + let action_repository = NativeActionRepository::connect( + arguments.repository_grpc.clone(), + arguments.identity_grpc.clone(), + arguments.identity_shared_secret.clone(), + ) + .await?; + let action_resolver = ActionResolver::new( + action_repository, + action_store.clone(), + PinnedOciResolver::new(arguments.action_oci_registries)?, + action_mirror, + action_allowlist, + ); + let native_repository = + NativeRepositoryContents::connect(arguments.repository_grpc.clone()).await?; + let repository_sources = RepositorySources::new( + native_repository, + RepositoryContents::new( + arguments.repository_source, + arguments.repository_source_token, + ), + ); + let intake = Arc::new(Intake::new( + repository_sources, + runs.clone(), + action_resolver, + arguments.event_feed_secret, + )); + let admin_token = arguments + .admin_token + .filter(|token| !token.is_empty()) + .ok_or("SYNCODE_CONTROL_ADMIN_TOKEN is required while serving")?; + let artifact_authority = ArtifactTokenAuthority::new(arguments.artifact_signing_key)?; + let http = router(intake) + .merge(admin_router(Arc::new( + Admin::new(runs.clone(), nodes.clone(), admin_token).with_operations(store.clone()), + ))) + .merge(action_delivery_router( + action_store, + artifact_authority.clone(), + runs.clone(), + )); + let events = TcpListener::bind(arguments.listen_events).await?; + let check_events = CheckEventPublisher::new( + store.clone(), + CheckEventPublisherConfig { + endpoint: arguments.collaboration_event_url.as_str(), + secret: arguments.collaboration_event_secret, + source_node_id: arguments.source_node_id, + interval: std::time::Duration::from_secs(arguments.check_event_interval_seconds), + batch: arguments.check_event_batch, + }, + )?; + + let authority = CapabilityAuthority::new(arguments.capability_signing_key)?; + let repositories = RepositoryCoordinates::new( + arguments.identity_grpc.clone(), + arguments.identity_shared_secret.clone(), + )?; + let secret_service = RuntimeSecrets::new( + runs.clone(), + nodes.clone(), + RepositorySecrets::new(arguments.secret_source, arguments.secret_source_token), + IdentityRepositoryCredentials::connect( + arguments.identity_grpc.clone(), + arguments.identity_shared_secret.clone(), + ) + .await?, + authority.clone(), + ); + let actions_read = ActionsRead::new( + runs.clone(), + IdentityActionsAuthorization::connect( + arguments.identity_grpc, + arguments.identity_shared_secret, + ) + .await?, + ); + + // Both ends of the control plane run for as long as the other does: without + // events there is nothing to assign, and without sessions there is nobody to + // assign it to. Whichever stops first takes the process down with it. + let node_service = match arguments.mode { + Mode::Shadow => NodeSessionServer::shadow( + runs.clone(), + nodes, + authority, + artifact_authority, + arguments.action_artifact_public_url, + repositories.clone(), + ), + Mode::Active => NodeSessionServer::new( + runs.clone(), + nodes, + authority, + artifact_authority, + arguments.action_artifact_public_url, + repositories, + ), + }; + tokio::select! { + served = axum::serve(events, http).into_future() => served?, + published = check_events.run() => published?, + served = Server::builder() + .add_service(GeneratedServer::new(node_service)) + .add_service(GeneratedSecretsServer::new(secret_service)) + .add_service(GeneratedChecksServer::new(Checks::new(runs.clone()))) + .add_service(GeneratedActionsReadServer::new(actions_read)) + .serve_with_shutdown(arguments.listen, shutdown()) => served?, + } + + Ok(()) +} + +async fn shutdown() { + if let Err(error) = tokio::signal::ctrl_c().await { + eprintln!("cannot listen for shutdown, keeping the service running: {error}"); + std::future::pending::<()>().await; + } +} diff --git a/tests/node_session.rs b/tests/node_session.rs --- a/tests/node_session.rs +++ b/tests/node_session.rs @@ -1,1644 +1,1531 @@ -#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)] - -#[path = "support/actions.rs"] -mod actions; - -use std::error::Error; -use std::time::{Duration, SystemTime}; - -use syncode_control::trigger::{Triggered, trigger}; -use syncode_control_node::wire::{ - Acknowledgement, Capabilities, Capacity, ControlMessage, Enrol, Heartbeat, Hello, JobProgress, - NodeMessage, control_message, node_message, -}; -use syncode_control_node::{ - ArtifactTokenAuthority, CapabilityAuthority, GeneratedServer, NodeSessionClient, - NodeSessionServer, ProjectionClient, -}; -use syncode_control_nodes::{CREDENTIAL_TERM, Ephemeral, Lifecycle, Nodes, Scope}; -use syncode_control_runs::{ - Conclusion, Fence, Forgotten, JobId, MatrixPolicy, Origin, Priority, QueuedJob, Requirements, - RunState, Runs, -}; -use syncode_workflow::{ - Event, EventKind, ExecutionPlan, PlanSchemaVersion, VersionedPlan, WorkflowCompiler, - WorkflowDialect, WorkflowSource, -}; -use syncode_workflow_github_actions::compiler::GithubActionsCompiler; -use syncode_workflow_github_actions::expression::ExpressionProgram; -use tokio::net::TcpListener; -use tokio::sync::mpsc; -use tokio_stream::StreamExt; -use tokio_stream::wrappers::{ReceiverStream, TcpListenerStream}; -use tonic::Streaming; -use tonic::transport::Server; - -use actions::FIXTURE_ACTIONS; - -type TestResult = Result>; - -const WORKFLOW: &str = r#" -name: CI -jobs: - build: - runs-on: [self-hosted, linux] - steps: - - run: echo "the plan came from the control plane" -"#; - -fn compile() -> ExecutionPlan { - let hir = GithubActionsCompiler - .compile(&WorkflowSource::new( - WorkflowDialect::GitHubActions, - WORKFLOW.as_bytes().to_vec(), - )) - .unwrap_or_else(|error| panic!("compile: {error}")); - syncode_workflow::plans(hir) - .unwrap_or_else(|error| panic!("lower: {error}")) - .into_iter() - .next() - .unwrap_or_else(|| panic!("the fixture declares a job")) -} - -struct Harness { - runs: Runs, - nodes: Nodes, - address: String, -} - -fn origin() -> Origin { - Origin::new( - "syncode/meta".to_owned(), - "a-commit".to_owned(), - "refs/heads/main".to_owned(), - "push".to_owned(), - ".gitea/workflows/ci.yml".to_owned(), - ) - .with_delivery(Some(format!("fixture-{}", uuid::Uuid::new_v4()))) -} - -fn repository_origin(repository: &str) -> Origin { - Origin::new( - repository.to_owned(), - "a-commit".to_owned(), - "refs/heads/main".to_owned(), - "push".to_owned(), - ".forgejo/workflows/ci.yml".to_owned(), - ) - .with_delivery(Some(format!("fixture-{}", uuid::Uuid::new_v4()))) -} - -fn capabilities() -> Capabilities { - Capabilities { - architecture: "arm64".to_owned(), - operating_system: "linux".to_owned(), - container_runtime: "docker".to_owned(), - container_runtime_version: "28.6.1".to_owned(), - cores: 2, - memory_bytes: 8 * 1024 * 1024 * 1024, - labels: vec!["self-hosted".to_owned(), "linux".to_owned()], - } -} - -fn capacity() -> Capacity { - Capacity { - build_volume_free_bytes: 60 * 1024 * 1024 * 1024, - layer_store_bytes: 12 * 1024 * 1024 * 1024, - cache_volume_present: true, - cache_volume_total_bytes: 100, - cache_volume_used_bytes: 40, - cache_volume_path: "/var/cache/syncode".to_owned(), - cached_images: Vec::new(), - cached_actions: Vec::new(), - } -} - -fn hello(sequence: u64, node: String, credential: String, capacity: Capacity) -> NodeMessage { - NodeMessage { - sequence, - message_id: format!("node-message-{sequence}"), - idempotency_key: format!("node-message-{sequence}"), - body: Some(node_message::Body::Hello(Hello { - node, - credential, - capabilities: Some(capabilities()), - capacity: Some(capacity), - max_parallel: 1, - })), - } -} - -/// What a node does before it can be given anything: spend a token, then open -/// a session with the credential it got back. -async fn enrolled( - harness: &Harness, - sender: &mpsc::Sender, - inbound: &mut Streaming, -) -> TestResult<(String, String)> { - let token = harness.nodes.issue_token(Scope::Instance).await?; - sender - .send(NodeMessage { - sequence: 1, - message_id: "node-message-1".to_owned(), - idempotency_key: "node-message-1".to_owned(), - body: Some(node_message::Body::Enrol(Enrol { - token: token.secret().expose().to_owned(), - })), - }) - .await?; - let message = next(inbound).await?; - let Some(control_message::Body::Enrolled(enrolled)) = message.body else { - panic!("expected an identity, got {:?}", message.body); - }; - Ok((enrolled.node, enrolled.credential)) -} - -async fn start() -> TestResult { - let (projection_base, mut projections) = projection_sink().await?; - tokio::spawn(async move { while projections.recv().await.is_some() {} }); - start_with_projection(projection_base).await -} - -async fn start_with_projection(projection_base: url::Url) -> TestResult { - let runs = Runs::restored(Forgotten::default()).await?; - let nodes = Nodes::restored(Ephemeral::default()).await?; - let listener = TcpListener::bind("127.0.0.1:0").await?; - let address = format!("http://{}", listener.local_addr()?); - let served = runs.clone(); - let served_nodes = nodes.clone(); - - tokio::spawn(async move { - let _ = Server::builder() - .add_service(GeneratedServer::new(NodeSessionServer::new( - served, - served_nodes, - CapabilityAuthority::new("test-capability-key").expect("capability key"), - ArtifactTokenAuthority::new("test-artifact-key").expect("artifact key"), - "http://127.0.0.1:1/".parse().expect("artifact URL"), - ProjectionClient::new( - projection_base, - String::new(), - "http://127.0.0.1:1".to_owned(), - String::new(), - ) - .expect("projection client"), - ))) - .serve_with_incoming(TcpListenerStream::new(listener)) - .await; - }); - - Ok(Harness { - runs, - nodes, - address, - }) -} - -/// Answers every projection push with 204 and hands back the bodies it saw, -/// in order. -async fn projection_sink() -> TestResult<(url::Url, mpsc::UnboundedReceiver)> { - let listener = TcpListener::bind("127.0.0.1:0").await?; - let base = url::Url::parse(&format!("http://{}/", listener.local_addr()?))?; - let (sender, receiver) = mpsc::unbounded_channel(); - - tokio::spawn(async move { - loop { - let Ok((mut stream, _)) = listener.accept().await else { - return; - }; - let mut buffer = vec![0_u8; 8192]; - let read = tokio::io::AsyncReadExt::read(&mut stream, &mut buffer) - .await - .unwrap_or(0); - let request = String::from_utf8_lossy(&buffer[..read]).to_string(); - let body = request - .split_once("\r\n\r\n") - .map(|(_, body)| body.to_owned()) - .unwrap_or_default(); - let _ = sender.send(body); - let response = "HTTP/1.1 204 X\r\ncontent-length: 0\r\nconnection: close\r\n\r\n"; - let _ = tokio::io::AsyncWriteExt::write_all(&mut stream, response.as_bytes()).await; - let _ = tokio::io::AsyncWriteExt::shutdown(&mut stream).await; - } - }); - - Ok((base, receiver)) -} - -async fn wait_until_offline(harness: &Harness, node: syncode_control_runs::NodeId) -> TestResult { - for _ in 0..100 { - if harness.nodes.lifecycle(node).await? == Lifecycle::Offline { - return Ok(()); - } - tokio::time::sleep(Duration::from_millis(10)).await; - } - Err("the closed node session did not become offline".into()) -} - -async fn next(stream: &mut Streaming) -> TestResult { - Ok(stream - .next() - .await - .ok_or("the control plane closed the stream")??) -} - -#[tokio::test] -async fn node_is_handed_the_plan_the_control_plane_compiled() -> TestResult { - let harness = start().await?; - let plan = VersionedPlan::new(compile()); - let run = harness - .runs - .queue(JobId::fresh(), origin(), serde_json::to_vec(&plan)?) - .await?; - - let (sender, receiver) = mpsc::channel(8); - let mut client = NodeSessionClient::connect(harness.address.clone()).await?; - let mut inbound = client - .open(ReceiverStream::new(receiver)) - .await? - .into_inner(); - - let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; - sender - .send(NodeMessage { - sequence: 2, - message_id: "node-message-2".to_owned(), - idempotency_key: "node-message-2".to_owned(), - body: Some(node_message::Body::Hello(Hello { - node: node.clone(), - credential, - capabilities: Some(capabilities()), - capacity: Some(capacity()), - max_parallel: 1, - })), - }) - .await?; - - let welcome = next(&mut inbound).await?; - assert!(matches!( - welcome.body, - Some(control_message::Body::Welcome(_)) - )); - assert_eq!(welcome.sequence, 2, "the stream numbers what it sends"); - - let assigned = next(&mut inbound).await?; - let Some(control_message::Body::Assignment(assignment)) = assigned.body else { - panic!("expected an assignment, got {:?}", assigned.body); - }; - assert_eq!(assignment.run, run.to_string()); - assert_eq!(assigned.sequence, 3); - - let carried: VersionedPlan = serde_json::from_slice(&assignment.plan)?; - assert_eq!(carried.schema(), PlanSchemaVersion::CURRENT); - assert_eq!(carried, plan); - assert_eq!(carried.plan().job().key().as_ref(), "build"); - - let RunState::Assigned(lease) = harness.runs.state_of(run).await? else { - panic!("the run must know who holds it before the assignment leaves"); - }; - assert_eq!(lease.node().to_string(), node); - assert_eq!(lease.fence().get(), assignment.fence); - - sender - .send(progress( - &run.to_string(), - &assignment.job, - assignment.fence, - Conclusion::Success, - )) - .await?; - - let mut state = harness.runs.state_of(run).await?; - for _ in 0..50 { - if matches!(state, RunState::Finished(_)) { - break; - } - tokio::time::sleep(std::time::Duration::from_millis(10)).await; - state = harness.runs.state_of(run).await?; - } - assert_eq!(state, RunState::Finished(Conclusion::Success)); - - Ok(()) -} - -/// The node's `actions_runtime_token` is only good for spending once the forge -/// has heard which node and fence hold the job it names — so that has to be -/// true by the time the assignment carrying the token reaches the node, not -/// just eventually. This harness never runs the periodic sweep, so a body can -/// only arrive here because dispatch pushed it itself. -#[tokio::test] -async fn dispatch_projects_the_assignment_before_the_node_can_act_on_it() -> TestResult { - let (projection_base, mut seen) = projection_sink().await?; - let harness = start_with_projection(projection_base).await?; - let plan = VersionedPlan::new(compile()); - // A projection is only meaningful once it can be traced back to the - // delivery that started the run, so only a delivery-bearing origin is - // ever pushed; that's exactly the shape a real webhook delivery has. - let origin = origin().with_delivery(Some("a-delivery".to_owned())); - let run = harness - .runs - .queue(JobId::fresh(), origin, serde_json::to_vec(&plan)?) - .await?; - - let (sender, receiver) = mpsc::channel(8); - let mut client = NodeSessionClient::connect(harness.address.clone()).await?; - let mut inbound = client - .open(ReceiverStream::new(receiver)) - .await? - .into_inner(); - - let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; - sender - .send(NodeMessage { - sequence: 2, - message_id: "node-message-2".to_owned(), - idempotency_key: "node-message-2".to_owned(), - body: Some(node_message::Body::Hello(Hello { - node: node.clone(), - credential, - capabilities: Some(capabilities()), - capacity: Some(capacity()), - max_parallel: 1, - })), - }) - .await?; - let _welcome = next(&mut inbound).await?; - - let assigned = next(&mut inbound).await?; - let Some(control_message::Body::Assignment(assignment)) = assigned.body else { - panic!("expected an assignment, got {:?}", assigned.body); - }; - - let pushed = tokio::time::timeout(Duration::from_secs(1), seen.recv()) - .await? - .ok_or("the projection endpoint was never called")?; - assert!( - pushed.contains(&run.to_string()), - "the pushed body must name the run the node was just handed: {pushed}" - ); - assert!( - pushed.contains(&format!("\"job\":\"{}\"", assignment.job)), - "the pushed body must name the assigned job: {pushed}" - ); - assert!( - pushed.contains("\"kind\":\"assigned\""), - "the forge must already see the job as assigned, not waiting: {pushed}" - ); - assert!( - pushed.contains(&format!("\"node\":\"{node}\"")), - "the forge must already know which node holds it: {pushed}" - ); - - Ok(()) -} - -#[tokio::test] -async fn cancellation_is_pushed_with_the_current_job_and_fence() -> TestResult { - let harness = start().await?; - let job = JobId::fresh(); - let run = harness - .runs - .queue( - job, - origin(), - serde_json::to_vec(&VersionedPlan::new(compile()))?, - ) - .await?; - let (sender, receiver) = mpsc::channel(8); - let mut client = NodeSessionClient::connect(harness.address.clone()).await?; - let mut inbound = client - .open(ReceiverStream::new(receiver)) - .await? - .into_inner(); - let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; - sender.send(hello(2, node, credential, capacity())).await?; - let _ = next(&mut inbound).await?; - let assignment_message = next(&mut inbound).await?; - let Some(control_message::Body::Assignment(assignment)) = assignment_message.body else { - return Err("expected an assignment".into()); - }; - - let dispatch = harness.runs.cancel_job(run, job).await?; - assert!(matches!( - dispatch, - syncode_control_runs::CancellationDispatch::Requested(_) - )); - let cancellation_message = - tokio::time::timeout(Duration::from_secs(1), next(&mut inbound)).await??; - let cancellation_sequence = cancellation_message.sequence; - let Some(control_message::Body::Cancel(cancellation)) = cancellation_message.body else { - return Err("expected a cancellation".into()); - }; - assert_eq!(cancellation.run, run.to_string()); - assert_eq!(cancellation.job, job.to_string()); - assert_eq!(cancellation.fence, assignment.fence); - - sender - .send(NodeMessage { - sequence: 3, - message_id: "cancel-ack".to_owned(), - idempotency_key: "cancel-ack".to_owned(), - body: Some(node_message::Body::Acknowledgement(Acknowledgement { - message_id: cancellation_message.message_id, - log_offset: 0, - acknowledged_outputs: Vec::new(), - conclusion: syncode_control_node::wire::Conclusion::Unspecified as i32, - })), - }) - .await?; - sender - .send(NodeMessage { - sequence: 4, - message_id: "heartbeat-after-cancel-ack".to_owned(), - idempotency_key: "heartbeat-after-cancel-ack".to_owned(), - body: Some(node_message::Body::Heartbeat(Heartbeat { - held: Vec::new(), - capacity: Some(capacity()), - })), - }) - .await?; - let heartbeat_acknowledgement = next(&mut inbound).await?; - assert_eq!( - heartbeat_acknowledgement.sequence, - cancellation_sequence + 1 - ); - assert!(matches!( - heartbeat_acknowledgement.body, - Some(control_message::Body::Acknowledgement(_)) - )); - - sender - .send(NodeMessage { - sequence: 5, - message_id: "cancelled-progress".to_owned(), - idempotency_key: "cancelled-progress".to_owned(), - body: Some(node_message::Body::Progress(JobProgress { - run: run.to_string(), - job: job.to_string(), - conclusion: syncode_control_node::wire::Conclusion::Cancelled as i32, - fence: assignment.fence, - outputs: Default::default(), - })), - }) - .await?; - - for _ in 0..50 { - if harness.runs.state_of(run).await? == RunState::Finished(Conclusion::Cancelled) { - return Ok(()); - } - tokio::time::sleep(Duration::from_millis(10)).await; - } - Err("cancelled progress did not finish the run".into()) -} - -#[tokio::test] -async fn a_job_requirement_mismatch_does_not_refuse_the_node() -> TestResult { - let harness = start().await?; - let event = Event::new( - EventKind::Push, - syncode_workflow::GitReference::Branch("main".to_owned()), - Vec::new(), - ); - let triggered = trigger( - &harness.runs, - br#" -on: [push] -jobs: - build: - runs-on: [self-hosted, linux, x64] - steps: - - run: cargo test -"#, - &event, - &origin(), - &FIXTURE_ACTIONS, - ) - .await?; - let Triggered::Runs(runs) = triggered else { - return Err("workflow did not trigger".into()); - }; - let run = *runs.first().ok_or("trigger returned no run")?; - - let (sender, receiver) = mpsc::channel(8); - let mut client = NodeSessionClient::connect(harness.address.clone()).await?; - let mut inbound = client - .open(ReceiverStream::new(receiver)) - .await? - .into_inner(); - let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; - sender - .send(NodeMessage { - sequence: 2, - message_id: "typed-hello".to_owned(), - idempotency_key: "typed-hello".to_owned(), - body: Some(node_message::Body::Hello(Hello { - node, - credential, - capabilities: Some(capabilities()), - capacity: Some(capacity()), - max_parallel: 1, - })), - }) - .await?; - - assert!(matches!( - next(&mut inbound).await?.body, - Some(control_message::Body::Welcome(_)) - )); - assert!( - tokio::time::timeout(Duration::from_millis(200), inbound.next()) - .await - .is_err(), - "a job requirement mismatch closed the healthy node session" - ); - assert_eq!(harness.runs.state_of(run).await?, RunState::Queued); - - harness - .runs - .queue( - JobId::fresh(), - origin(), - serde_json::to_vec(&VersionedPlan::new(compile()))?, - ) - .await?; - let assignment = tokio::time::timeout(Duration::from_secs(1), next(&mut inbound)).await??; - assert!(matches!( - assignment.body, - Some(control_message::Body::Assignment(_)) - )); - Ok(()) -} - -#[tokio::test] -async fn stream_dispatch_does_not_let_one_organization_monopolize_capacity() -> TestResult { - let harness = start().await?; - let plan = serde_json::to_vec(&VersionedPlan::new(compile()))?; - for _ in 0..3 { - harness - .runs - .queue( - JobId::fresh(), - repository_origin("backlog/project"), - plan.clone(), - ) - .await?; - } - harness - .runs - .queue(JobId::fresh(), repository_origin("neighbor/project"), plan) - .await?; - - let (sender, receiver) = mpsc::channel(8); - let mut client = NodeSessionClient::connect(harness.address.clone()).await?; - let mut inbound = client - .open(ReceiverStream::new(receiver)) - .await? - .into_inner(); - let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; - sender - .send(NodeMessage { - sequence: 2, - message_id: "fair-hello".to_owned(), - idempotency_key: "fair-hello".to_owned(), - body: Some(node_message::Body::Hello(Hello { - node, - credential, - capabilities: Some(capabilities()), - capacity: Some(capacity()), - max_parallel: 2, - })), - }) - .await?; - let _ = next(&mut inbound).await?; - let first = next(&mut inbound).await?; - let second = next(&mut inbound).await?; - let Some(control_message::Body::Assignment(first)) = first.body else { - return Err("first message was not an assignment".into()); - }; - let Some(control_message::Body::Assignment(second)) = second.body else { - return Err("second message was not an assignment".into()); - }; - assert_eq!( - first - .origin - .ok_or("first assignment has no origin")? - .repository, - "backlog/project" - ); - assert_eq!( - second - .origin - .ok_or("second assignment has no origin")? - .repository, - "neighbor/project" - ); - Ok(()) -} - -#[tokio::test] -async fn stream_dispatch_selects_the_warmest_connected_node() -> TestResult { - let harness = start().await?; - let (cold_sender, cold_receiver) = mpsc::channel(8); - let mut cold_client = NodeSessionClient::connect(harness.address.clone()).await?; - let mut cold_inbound = cold_client - .open(ReceiverStream::new(cold_receiver)) - .await? - .into_inner(); - let (cold_node, cold_credential) = enrolled(&harness, &cold_sender, &mut cold_inbound).await?; - let mut cold_capacity = capacity(); - cold_capacity.cache_volume_present = false; - cold_capacity.cached_images.clear(); - cold_capacity.cached_actions.clear(); - cold_sender - .send(hello(2, cold_node, cold_credential, cold_capacity)) - .await?; - assert!(matches!( - next(&mut cold_inbound).await?.body, - Some(control_message::Body::Welcome(_)) - )); - - let (warm_sender, warm_receiver) = mpsc::channel(8); - let mut warm_client = NodeSessionClient::connect(harness.address.clone()).await?; - let mut warm_inbound = warm_client - .open(ReceiverStream::new(warm_receiver)) - .await? - .into_inner(); - let (warm_node, warm_credential) = enrolled(&harness, &warm_sender, &mut warm_inbound).await?; - let mut warm_capacity = capacity(); - warm_capacity.cached_images = vec!["postgres:18".to_owned()]; - warm_capacity.cached_actions = vec!["actions/checkout@v5".to_owned()]; - warm_sender - .send(hello(2, warm_node, warm_credential, warm_capacity)) - .await?; - assert!(matches!( - next(&mut warm_inbound).await?.body, - Some(control_message::Body::Welcome(_)) - )); - - harness - .runs - .queue_run( - origin(), - vec![ - QueuedJob::new( - JobId::fresh(), - "build".to_owned(), - Vec::new(), - MatrixPolicy::default(), - vec![1], - ) - .scheduled( - Priority::Normal, - Requirements::default().prefer( - ["postgres:18".to_owned()], - ["actions/checkout@v5".to_owned()], - ), - ), - ], - ) - .await?; - - let message = tokio::time::timeout(Duration::from_secs(2), next(&mut warm_inbound)).await??; - assert!(matches!( - message.body, - Some(control_message::Body::Assignment(_)) - )); - assert!( - tokio::time::timeout(Duration::from_millis(200), next(&mut cold_inbound)) - .await - .is_err() - ); - Ok(()) -} - -#[tokio::test] -async fn a_node_receives_only_its_declared_number_of_concurrent_jobs() -> TestResult { - let harness = start().await?; - let plan = serde_json::to_vec(&VersionedPlan::new(compile()))?; - harness - .runs - .queue(JobId::fresh(), origin(), plan.clone()) - .await?; - harness - .runs - .queue(JobId::fresh(), origin(), plan.clone()) - .await?; - - let (sender, receiver) = mpsc::channel(8); - let mut client = NodeSessionClient::connect(harness.address.clone()).await?; - let mut inbound = client - .open(ReceiverStream::new(receiver)) - .await? - .into_inner(); - let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; - sender - .send(NodeMessage { - sequence: 2, - message_id: "node-message-2".to_owned(), - idempotency_key: "node-message-2".to_owned(), - body: Some(node_message::Body::Hello(Hello { - node: node.clone(), - credential, - capabilities: Some(capabilities()), - capacity: Some(capacity()), - max_parallel: 2, - })), - }) - .await?; - - assert!(matches!( - next(&mut inbound).await?.body, - Some(control_message::Body::Welcome(_)) - )); - let first = next(&mut inbound).await?; - let second = next(&mut inbound).await?; - let Some(control_message::Body::Assignment(first)) = first.body else { - panic!("expected the first assignment"); - }; - let Some(control_message::Body::Assignment(second)) = second.body else { - panic!("expected the second assignment"); - }; - - harness.runs.queue(JobId::fresh(), origin(), plan).await?; - assert!( - tokio::time::timeout(Duration::from_millis(100), inbound.next()) - .await - .is_err(), - "a full node received a third concurrent job" - ); - - let completed = progress(&first.run, &first.job, first.fence, Conclusion::Success); - sender.send(completed.clone()).await?; - sender.send(completed).await?; - let replacement = next(&mut inbound).await?; - assert!(matches!( - replacement.body, - Some(control_message::Body::Assignment(_)) - )); - - sender - .send(NodeMessage { - sequence: 4, - message_id: "node-message-4".to_owned(), - idempotency_key: "node-message-4".to_owned(), - body: Some(node_message::Body::Progress(JobProgress { - run: second.run, - job: second.job, - conclusion: syncode_control_node::wire::Conclusion::Success.into(), - fence: second.fence, - outputs: Default::default(), - })), - }) - .await?; - Ok(()) -} - -#[tokio::test] -async fn an_out_of_order_node_message_closes_the_runtime_session() -> TestResult { - let harness = start().await?; - let (sender, receiver) = mpsc::channel(8); - let mut client = NodeSessionClient::connect(harness.address.clone()).await?; - let mut inbound = client - .open(ReceiverStream::new(receiver)) - .await? - .into_inner(); - let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; - sender - .send(NodeMessage { - sequence: 3, - message_id: "node-message-3".to_owned(), - idempotency_key: "node-message-3".to_owned(), - body: Some(node_message::Body::Hello(Hello { - node, - credential, - capabilities: Some(capabilities()), - capacity: Some(capacity()), - max_parallel: 1, - })), - }) - .await?; - - let error = next(&mut inbound) - .await - .expect_err("the skipped sequence must close the stream"); - assert!(error.to_string().contains("while 2 was required")); - Ok(()) -} - -#[tokio::test] -async fn work_queued_after_hello_is_pushed_without_another_heartbeat() -> TestResult { - let harness = start().await?; - let (sender, receiver) = mpsc::channel(8); - let mut client = NodeSessionClient::connect(harness.address.clone()).await?; - let mut inbound = client - .open(ReceiverStream::new(receiver)) - .await? - .into_inner(); - let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; - sender - .send(NodeMessage { - sequence: 2, - message_id: "node-message-2".to_owned(), - idempotency_key: "node-message-2".to_owned(), - body: Some(node_message::Body::Hello(Hello { - node, - credential, - capabilities: Some(capabilities()), - capacity: Some(capacity()), - max_parallel: 1, - })), - }) - .await?; - assert!(matches!( - next(&mut inbound).await?.body, - Some(control_message::Body::Welcome(_)) - )); - - harness - .runs - .queue( - JobId::fresh(), - origin(), - serde_json::to_vec(&VersionedPlan::new(compile()))?, - ) - .await?; - let pushed = tokio::time::timeout(Duration::from_secs(1), next(&mut inbound)).await??; - assert!(matches!( - pushed.body, - Some(control_message::Body::Assignment(_)) - )); - Ok(()) -} - -/// A node that keeps beating is handed its next credential before the one it -/// holds runs out, and the new one is what opens the session after that. Without -/// this a node works for exactly one credential term and is then locked out of -/// the control plane for good, with no way back but a fresh enrolment token. -#[tokio::test] -async fn a_credential_near_expiry_is_replaced_over_the_open_session() -> TestResult { - let harness = start().await?; - let (sender, receiver) = mpsc::channel(8); - let mut client = NodeSessionClient::connect(harness.address.clone()).await?; - let mut inbound = client - .open(ReceiverStream::new(receiver)) - .await? - .into_inner(); - - let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; - sender - .send(NodeMessage { - sequence: 2, - message_id: "node-message-2".to_owned(), - idempotency_key: "node-message-2".to_owned(), - body: Some(node_message::Body::Hello(Hello { - node: node.clone(), - credential: credential.clone(), - capabilities: Some(capabilities()), - capacity: Some(capacity()), - max_parallel: 1, - })), - }) - .await?; - - let welcome = next(&mut inbound).await?; - assert!( - matches!(welcome.body, Some(control_message::Body::Welcome(_))), - "a fresh credential is not renewed, got {:?}", - welcome.body - ); - - // Age the credential by issuing it as though the term had nearly run out. - // Sixty seconds of life left is inside the renewal margin, so the next thing - // the node says has to be answered with a replacement. - let issued = SystemTime::now() - CREDENTIAL_TERM + Duration::from_secs(60); - let ageing = harness.nodes.rotate(node.parse()?, issued).await?; - sender - .send(NodeMessage { - sequence: 3, - message_id: "node-message-3".to_owned(), - idempotency_key: "node-message-3".to_owned(), - body: Some(node_message::Body::Heartbeat(Heartbeat { - held: Vec::new(), - capacity: Some(capacity()), - })), - }) - .await?; - - let answer = next(&mut inbound).await?; - let Some(control_message::Body::Rotated(rotated)) = answer.body else { - panic!("expected a replacement credential, got {:?}", answer.body); - }; - assert_ne!( - rotated.credential, - ageing.secret().expose(), - "a rotation that hands back the same credential renews nothing" - ); - assert_ne!(rotated.credential, credential); - let acknowledged = next(&mut inbound).await?; - assert!(matches!( - acknowledged.body, - Some(control_message::Body::Acknowledgement(_)) - )); - - sender - .send(NodeMessage { - sequence: 4, - message_id: "node-message-4".to_owned(), - idempotency_key: "node-message-4".to_owned(), - body: Some(node_message::Body::Heartbeat(Heartbeat { - held: Vec::new(), - capacity: Some(capacity()), - })), - }) - .await?; - let repeated = next(&mut inbound).await?; - let Some(control_message::Body::Rotated(repeated)) = repeated.body else { - panic!("expected the unconfirmed credential again"); - }; - assert_eq!(repeated.credential, rotated.credential); - - drop(sender); - drop(inbound); - wait_until_offline(&harness, node.parse()?).await?; - - // The point of the exercise: the credential the node was pushed is the one - // that gets it back in, and the one it replaced does not. - let (fresh_sender, fresh_receiver) = mpsc::channel(8); - let mut reopened = client - .open(ReceiverStream::new(fresh_receiver)) - .await? - .into_inner(); - fresh_sender - .send(NodeMessage { - sequence: 1, - message_id: "node-message-1".to_owned(), - idempotency_key: "node-message-1".to_owned(), - body: Some(node_message::Body::Hello(Hello { - node: node.clone(), - credential: rotated.credential, - capabilities: Some(capabilities()), - capacity: Some(capacity()), - max_parallel: 1, - })), - }) - .await?; - assert!(matches!( - next(&mut reopened).await?.body, - Some(control_message::Body::Welcome(_)) - )); - - let (stale_sender, stale_receiver) = mpsc::channel(8); - let mut refused = client - .open(ReceiverStream::new(stale_receiver)) - .await? - .into_inner(); - stale_sender - .send(NodeMessage { - sequence: 1, - message_id: "node-message-1".to_owned(), - idempotency_key: "node-message-1".to_owned(), - body: Some(node_message::Body::Hello(Hello { - node, - credential, - capabilities: Some(capabilities()), - capacity: Some(capacity()), - max_parallel: 1, - })), - }) - .await?; - let error = next(&mut refused) - .await - .expect_err("the credential that was replaced must stop working"); - assert!( - error.to_string().contains("not current"), - "unexpected error: {error}" - ); - - Ok(()) -} - -/// An identity handed out to something that never arrived has to settle, or the -/// sweep reports it as newly gone every fifteen seconds forever and the log -/// stops being worth reading. -#[tokio::test] -async fn a_node_that_enrolled_and_never_spoke_is_reported_gone_once() -> TestResult { - let harness = start().await?; - let token = harness.nodes.issue_token(Scope::Instance).await?; - let now = SystemTime::now(); - let (node, _) = harness.nodes.enrol(token.secret().expose(), now).await?; - - let deadline = Duration::from_secs(90); - let late = now + Duration::from_secs(600); - assert_eq!( - harness.nodes.sweep_silent(deadline, late).await?, - vec![node] - ); - assert!( - harness.nodes.sweep_silent(deadline, late).await?.is_empty(), - "a node already marked offline is not gone again" - ); - - Ok(()) -} - -fn progress(run: &str, job: &str, fence: u64, conclusion: Conclusion) -> NodeMessage { - let conclusion = match conclusion { - Conclusion::Success => syncode_control_node::wire::Conclusion::Success, - Conclusion::Failure => syncode_control_node::wire::Conclusion::Failure, - Conclusion::Cancelled => syncode_control_node::wire::Conclusion::Cancelled, - Conclusion::Skipped => syncode_control_node::wire::Conclusion::Skipped, - }; - NodeMessage { - sequence: 3, - message_id: "node-message-3".to_owned(), - idempotency_key: "node-message-3".to_owned(), - body: Some(node_message::Body::Progress(JobProgress { - run: run.to_owned(), - job: job.to_owned(), - conclusion: conclusion.into(), - fence, - outputs: Default::default(), - })), - } -} - -#[tokio::test] -async fn report_stating_a_grant_that_is_not_current_is_refused() -> TestResult { - let harness = start().await?; - let plan = VersionedPlan::new(compile()); - let run = harness - .runs - .queue(JobId::fresh(), origin(), serde_json::to_vec(&plan)?) - .await?; - - let holder = syncode_control_runs::NodeId::fresh(); - let assignment = harness - .runs - .take_next(holder) - .await? - .ok_or("nothing was queued")?; - - let error = harness - .runs - .finished( - run, - holder, - Fence::from(assignment.fence().get() + 1), - Conclusion::Success, - ) - .await - .expect_err("a grant nobody was given must not conclude a run"); - - assert!( - error.to_string().contains("not the current grant"), - "unexpected error: {error}" - ); - assert!(matches!( - harness.runs.state_of(run).await?, - RunState::Assigned(_) - )); - - Ok(()) -} - -#[tokio::test] -async fn a_session_without_a_credential_is_refused() -> TestResult { - let harness = start().await?; - let (sender, receiver) = mpsc::channel(8); - let mut client = NodeSessionClient::connect(harness.address.clone()).await?; - let mut inbound = client - .open(ReceiverStream::new(receiver)) - .await? - .into_inner(); - - let (node, _) = enrolled(&harness, &sender, &mut inbound).await?; - sender - .send(NodeMessage { - sequence: 2, - message_id: "node-message-2".to_owned(), - idempotency_key: "node-message-2".to_owned(), - body: Some(node_message::Body::Hello(Hello { - node, - credential: "not the one it was given".to_owned(), - capabilities: Some(capabilities()), - capacity: Some(capacity()), - max_parallel: 1, - })), - }) - .await?; - - let error = next(&mut inbound) - .await - .expect_err("a wrong credential must end the session"); - assert!( - error.to_string().contains("not current"), - "unexpected error: {error}" - ); - - Ok(()) -} - -#[tokio::test] -async fn a_node_that_does_not_say_how_much_room_it_has_is_refused() -> TestResult { - let harness = start().await?; - let (sender, receiver) = mpsc::channel(8); - let mut client = NodeSessionClient::connect(harness.address.clone()).await?; - let mut inbound = client - .open(ReceiverStream::new(receiver)) - .await? - .into_inner(); - - let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; - sender - .send(NodeMessage { - sequence: 2, - message_id: "node-message-2".to_owned(), - idempotency_key: "node-message-2".to_owned(), - body: Some(node_message::Body::Hello(Hello { - node, - credential, - capabilities: Some(capabilities()), - capacity: None, - max_parallel: 1, - })), - }) - .await?; - - let error = next(&mut inbound) - .await - .expect_err("a node without capacity must not open a session"); - assert!( - error.to_string().contains("how much room"), - "unexpected error: {error}" - ); - - Ok(()) -} - -#[tokio::test] -async fn a_failed_capacity_refresh_pauses_work_without_closing_the_session() -> TestResult { - let harness = start().await?; - let (sender, receiver) = mpsc::channel(8); - let mut client = NodeSessionClient::connect(harness.address.clone()).await?; - let mut inbound = client - .open(ReceiverStream::new(receiver)) - .await? - .into_inner(); - let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; - sender - .send(NodeMessage { - sequence: 2, - message_id: "node-message-2".to_owned(), - idempotency_key: "node-message-2".to_owned(), - body: Some(node_message::Body::Hello(Hello { - node: node.clone(), - credential, - capabilities: Some(capabilities()), - capacity: Some(capacity()), - max_parallel: 1, - })), - }) - .await?; - assert!(matches!( - next(&mut inbound).await?.body, - Some(control_message::Body::Welcome(_)) - )); - - sender - .send(NodeMessage { - sequence: 3, - message_id: "node-message-3".to_owned(), - idempotency_key: "node-message-3".to_owned(), - body: Some(node_message::Body::Heartbeat(Heartbeat { - held: Vec::new(), - capacity: None, - })), - }) - .await?; - let node_id = node.parse()?; - for _ in 0..50 { - if !harness.nodes.employable(node_id).await? { - break; - } - tokio::time::sleep(Duration::from_millis(10)).await; - } - assert!(!harness.nodes.employable(node_id).await?); - assert!(matches!( - next(&mut inbound).await?.body, - Some(control_message::Body::Acknowledgement(_)) - )); - harness - .runs - .queue( - JobId::fresh(), - origin(), - serde_json::to_vec(&VersionedPlan::new(compile()))?, - ) - .await?; - - assert!( - tokio::time::timeout(Duration::from_millis(100), next(&mut inbound)) - .await - .is_err(), - "an unmeasured node must stay connected without receiving work" - ); - - sender - .send(NodeMessage { - sequence: 4, - message_id: "node-message-4".to_owned(), - idempotency_key: "node-message-4".to_owned(), - body: Some(node_message::Body::Heartbeat(Heartbeat { - held: Vec::new(), - capacity: Some(capacity()), - })), - }) - .await?; - let answer = next(&mut inbound).await?; - assert!( - matches!(answer.body, Some(control_message::Body::Assignment(_))), - "a recovered capacity measurement must resume work" - ); - Ok(()) -} - -#[tokio::test] -async fn a_node_short_of_room_is_told_why_instead_of_being_given_work() -> TestResult { - let harness = start().await?; - let plan = VersionedPlan::new(compile()); - harness - .runs - .queue(JobId::fresh(), origin(), serde_json::to_vec(&plan)?) - .await?; - - let (sender, receiver) = mpsc::channel(8); - let mut client = NodeSessionClient::connect(harness.address.clone()).await?; - let mut inbound = client - .open(ReceiverStream::new(receiver)) - .await? - .into_inner(); - - let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; - let cramped = Capacity { - build_volume_free_bytes: 1024, - ..capacity() - }; - sender - .send(NodeMessage { - sequence: 2, - message_id: "node-message-2".to_owned(), - idempotency_key: "node-message-2".to_owned(), - body: Some(node_message::Body::Hello(Hello { - node, - credential, - capabilities: Some(capabilities()), - capacity: Some(cramped), - max_parallel: 1, - })), - }) - .await?; - - let welcome = next(&mut inbound).await?; - assert!(matches!( - welcome.body, - Some(control_message::Body::Welcome(_)) - )); - - let answer = next(&mut inbound).await?; - let Some(control_message::Body::Refused(refused)) = answer.body else { - panic!("expected a stated refusal, got {:?}", answer.body); - }; - assert!( - refused.reason.contains("below the"), - "unexpected reason: {}", - refused.reason - ); - - Ok(()) -} - -#[tokio::test] -async fn an_applied_message_is_deduplicated_after_reconnect() -> TestResult { - let harness = start().await?; - let (first_sender, first_receiver) = mpsc::channel(8); - let mut client = NodeSessionClient::connect(harness.address.clone()).await?; - let mut first = client - .open(ReceiverStream::new(first_receiver)) - .await? - .into_inner(); - let (node, credential) = enrolled(&harness, &first_sender, &mut first).await?; - first_sender - .send(NodeMessage { - sequence: 2, - message_id: "first-hello".to_owned(), - idempotency_key: "first-hello".to_owned(), - body: Some(node_message::Body::Hello(Hello { - node: node.clone(), - credential: credential.clone(), - capabilities: Some(capabilities()), - capacity: Some(capacity()), - max_parallel: 1, - })), - }) - .await?; - let _ = next(&mut first).await?; - let repeated = NodeMessage { - sequence: 3, - message_id: "persistent-heartbeat".to_owned(), - idempotency_key: "persistent-heartbeat".to_owned(), - body: Some(node_message::Body::Heartbeat(Heartbeat { - held: Vec::new(), - capacity: None, - })), - }; - first_sender.send(repeated.clone()).await?; - let node_id = node.parse()?; - for _ in 0..50 { - if !harness.nodes.employable(node_id).await? { - break; - } - tokio::time::sleep(Duration::from_millis(10)).await; - } - assert!(!harness.nodes.employable(node_id).await?); - - drop(first_sender); - drop(first); - wait_until_offline(&harness, node_id).await?; - - let (second_sender, second_receiver) = mpsc::channel(8); - let mut second_client = NodeSessionClient::connect(harness.address.clone()).await?; - let mut second = second_client - .open(ReceiverStream::new(second_receiver)) - .await? - .into_inner(); - second_sender - .send(NodeMessage { - sequence: 1, - message_id: "second-hello".to_owned(), - idempotency_key: "second-hello".to_owned(), - body: Some(node_message::Body::Hello(Hello { - node, - credential, - capabilities: Some(capabilities()), - capacity: Some(capacity()), - max_parallel: 1, - })), - }) - .await?; - let _ = next(&mut second).await?; - assert!(harness.nodes.employable(node_id).await?); - - second_sender - .send(NodeMessage { - sequence: 2, - ..repeated - }) - .await?; - tokio::time::sleep(Duration::from_millis(50)).await; - assert!( - harness.nodes.employable(node_id).await?, - "the duplicate must not clear capacity again" - ); - - second_sender - .send(NodeMessage { - sequence: 3, - message_id: "new-heartbeat".to_owned(), - idempotency_key: "new-heartbeat".to_owned(), - body: Some(node_message::Body::Heartbeat(Heartbeat { - held: Vec::new(), - capacity: None, - })), - }) - .await?; - for _ in 0..50 { - if !harness.nodes.employable(node_id).await? { - break; - } - tokio::time::sleep(Duration::from_millis(10)).await; - } - assert!(!harness.nodes.employable(node_id).await?); - Ok(()) -} - -#[tokio::test] -async fn an_unacknowledged_assignment_is_retried_on_the_open_session() -> TestResult { - let harness = start().await?; - harness - .runs - .queue( - JobId::fresh(), - origin(), - serde_json::to_vec(&VersionedPlan::new(compile()))?, - ) - .await?; - - let (sender, receiver) = mpsc::channel(8); - let mut client = NodeSessionClient::connect(harness.address.clone()).await?; - let mut inbound = client - .open(ReceiverStream::new(receiver)) - .await? - .into_inner(); - let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; - sender - .send(NodeMessage { - sequence: 2, - message_id: "hello".to_owned(), - idempotency_key: "hello".to_owned(), - body: Some(node_message::Body::Hello(Hello { - node: node.clone(), - credential, - capabilities: Some(capabilities()), - capacity: Some(capacity()), - max_parallel: 1, - })), - }) - .await?; - let _ = next(&mut inbound).await?; - let assignment = next(&mut inbound).await?; - assert!(matches!( - assignment.body, - Some(control_message::Body::Assignment(_)) - )); - - let retried = tokio::time::timeout(Duration::from_secs(3), next(&mut inbound)).await??; - assert_eq!(retried, assignment); - - sender - .send(NodeMessage { - sequence: 3, - message_id: "assignment-ack".to_owned(), - idempotency_key: "assignment-ack".to_owned(), - body: Some(node_message::Body::Acknowledgement(Acknowledgement { - message_id: assignment.message_id.clone(), - log_offset: 0, - acknowledged_outputs: Vec::new(), - conclusion: 0, - })), - }) - .await?; - let node = node.parse()?; - for _ in 0..50 { - if harness.nodes.outbound_pending(node).await?.is_empty() { - break; - } - tokio::time::sleep(Duration::from_millis(10)).await; - } - assert!(harness.nodes.outbound_pending(node).await?.is_empty()); - - let acknowledgement = Acknowledgement { - message_id: assignment.message_id, - log_offset: 0, - acknowledged_outputs: Vec::new(), - conclusion: 0, - }; - sender - .send(NodeMessage { - sequence: 4, - message_id: "retried-assignment-ack".to_owned(), - idempotency_key: "assignment-ack".to_owned(), - body: Some(node_message::Body::Acknowledgement(acknowledgement)), - }) - .await?; - sender - .send(NodeMessage { - sequence: 5, - message_id: "heartbeat-after-retried-ack".to_owned(), - idempotency_key: "heartbeat-after-retried-ack".to_owned(), - body: Some(node_message::Body::Heartbeat(Heartbeat { - held: Vec::new(), - capacity: Some(capacity()), - })), - }) - .await?; - assert!(matches!( - next(&mut inbound).await?.body, - Some(control_message::Body::Acknowledgement(_)) - )); - Ok(()) -} - -#[tokio::test] -async fn reconnect_does_not_restore_capacity_while_a_lease_is_held() -> TestResult { - let harness = start().await?; - let plan = serde_json::to_vec(&VersionedPlan::new(compile()))?; - let first_run = harness - .runs - .queue(JobId::fresh(), origin(), plan.clone()) - .await?; - let second_run = harness.runs.queue(JobId::fresh(), origin(), plan).await?; - let (sender, receiver) = mpsc::channel(8); - let mut client = NodeSessionClient::connect(harness.address.clone()).await?; - let mut inbound = client - .open(ReceiverStream::new(receiver)) - .await? - .into_inner(); - let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; - sender - .send(hello(2, node.clone(), credential.clone(), capacity())) - .await?; - let _ = next(&mut inbound).await?; - let assigned = next(&mut inbound).await?; - let Some(control_message::Body::Assignment(assignment)) = assigned.body.clone() else { - return Err("expected the first assignment".into()); - }; - assert_eq!(assignment.run, first_run.to_string()); - sender - .send(NodeMessage { - sequence: 3, - message_id: "assignment-ack".to_owned(), - idempotency_key: "assignment-ack".to_owned(), - body: Some(node_message::Body::Acknowledgement(Acknowledgement { - message_id: assigned.message_id, - log_offset: 0, - acknowledged_outputs: Vec::new(), - conclusion: 0, - })), - }) - .await?; - let node_id = node.parse()?; - for _ in 0..50 { - if harness.nodes.outbound_pending(node_id).await?.is_empty() { - break; - } - tokio::time::sleep(Duration::from_millis(10)).await; - } - drop(sender); - drop(inbound); - wait_until_offline(&harness, node_id).await?; - - let (sender, receiver) = mpsc::channel(8); - let mut reopened = client - .open(ReceiverStream::new(receiver)) - .await? - .into_inner(); - sender.send(hello(1, node, credential, capacity())).await?; - let _ = next(&mut reopened).await?; - assert!( - tokio::time::timeout(Duration::from_millis(200), next(&mut reopened)) - .await - .is_err(), - "reconnect restored a slot still occupied by the first lease" - ); - - harness - .runs - .finished_job( - first_run, - assignment.job.parse()?, - node_id, - Fence::from(assignment.fence), - Conclusion::Success, - ) - .await?; - let next_assignment = - tokio::time::timeout(Duration::from_secs(1), next(&mut reopened)).await??; - let Some(control_message::Body::Assignment(assignment)) = next_assignment.body else { - return Err("expected the second assignment".into()); - }; - assert_eq!(assignment.run, second_run.to_string()); - Ok(()) -} - -#[tokio::test] -async fn a_silent_open_session_is_refused_after_its_lease_expires() -> TestResult { - let harness = start().await?; - let plan = serde_json::to_vec(&VersionedPlan::new(compile()))?; - let run = harness.runs.queue(JobId::fresh(), origin(), plan).await?; - let (sender, receiver) = mpsc::channel(8); - let mut client = NodeSessionClient::connect(harness.address.clone()).await?; - let mut inbound = client - .open(ReceiverStream::new(receiver)) - .await? - .into_inner(); - let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; - sender - .send(hello(2, node.clone(), credential, capacity())) - .await?; - let _ = next(&mut inbound).await?; - let assignment = next(&mut inbound).await?; - assert!(matches!( - assignment.body, - Some(control_message::Body::Assignment(_)) - )); - - let node_id = node.parse()?; - let late = SystemTime::now() + Duration::from_secs(600); - assert_eq!( - harness - .nodes - .sweep_silent(Duration::from_secs(90), late) - .await?, - vec![node_id] - ); - assert_eq!(harness.nodes.lifecycle(node_id).await?, Lifecycle::Offline); - assert_eq!(harness.runs.reclaim_expired(late).await?, vec![run]); - - let refused = tokio::time::timeout(Duration::from_secs(1), next(&mut inbound)).await??; - assert!(matches!( - refused.body, - Some(control_message::Body::Refused(_)) - )); - assert_eq!(harness.runs.state_of(run).await?, RunState::Queued); - Ok(()) -} +#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)] + +#[path = "support/actions.rs"] +mod actions; + +use std::error::Error; +use std::time::{Duration, SystemTime}; + +use syncode_control::trigger::{Triggered, trigger}; +use syncode_control_node::wire::{ + Acknowledgement, Capabilities, Capacity, ControlMessage, Enrol, Heartbeat, Hello, JobProgress, + NodeMessage, control_message, node_message, +}; +use syncode_control_node::{ + ArtifactTokenAuthority, CapabilityAuthority, GeneratedServer, NodeSessionClient, + NodeSessionServer, RepositoryCoordinates, +}; +use syncode_control_nodes::{CREDENTIAL_TERM, Ephemeral, Lifecycle, Nodes, Scope}; +use syncode_control_runs::{ + Conclusion, Fence, Forgotten, JobId, MatrixPolicy, Origin, Priority, QueuedJob, Requirements, + RunState, Runs, +}; +use syncode_workflow::{ + Event, EventKind, ExecutionPlan, PlanSchemaVersion, VersionedPlan, WorkflowCompiler, + WorkflowDialect, WorkflowSource, +}; +use syncode_workflow_github_actions::compiler::GithubActionsCompiler; +use syncode_workflow_github_actions::expression::ExpressionProgram; +use tokio::net::TcpListener; +use tokio::sync::mpsc; +use tokio_stream::StreamExt; +use tokio_stream::wrappers::{ReceiverStream, TcpListenerStream}; +use tonic::Streaming; +use tonic::transport::Server; + +use actions::FIXTURE_ACTIONS; + +type TestResult = Result>; + +const WORKFLOW: &str = r#" +name: CI +jobs: + build: + runs-on: [self-hosted, linux] + steps: + - run: echo "the plan came from the control plane" +"#; + +fn compile() -> ExecutionPlan { + let hir = GithubActionsCompiler + .compile(&WorkflowSource::new( + WorkflowDialect::GitHubActions, + WORKFLOW.as_bytes().to_vec(), + )) + .unwrap_or_else(|error| panic!("compile: {error}")); + syncode_workflow::plans(hir) + .unwrap_or_else(|error| panic!("lower: {error}")) + .into_iter() + .next() + .unwrap_or_else(|| panic!("the fixture declares a job")) +} + +struct Harness { + runs: Runs, + nodes: Nodes, + address: String, +} + +fn origin() -> Origin { + Origin::new( + "syncode/meta".to_owned(), + "a-commit".to_owned(), + "refs/heads/main".to_owned(), + "push".to_owned(), + ".gitea/workflows/ci.yml".to_owned(), + ) + .with_delivery(Some(format!("fixture-{}", uuid::Uuid::new_v4()))) +} + +fn repository_origin(repository: &str) -> Origin { + Origin::new( + repository.to_owned(), + "a-commit".to_owned(), + "refs/heads/main".to_owned(), + "push".to_owned(), + ".forgejo/workflows/ci.yml".to_owned(), + ) + .with_delivery(Some(format!("fixture-{}", uuid::Uuid::new_v4()))) +} + +fn capabilities() -> Capabilities { + Capabilities { + architecture: "arm64".to_owned(), + operating_system: "linux".to_owned(), + container_runtime: "docker".to_owned(), + container_runtime_version: "28.6.1".to_owned(), + cores: 2, + memory_bytes: 8 * 1024 * 1024 * 1024, + labels: vec!["self-hosted".to_owned(), "linux".to_owned()], + } +} + +fn capacity() -> Capacity { + Capacity { + build_volume_free_bytes: 60 * 1024 * 1024 * 1024, + layer_store_bytes: 12 * 1024 * 1024 * 1024, + cache_volume_present: true, + cache_volume_total_bytes: 100, + cache_volume_used_bytes: 40, + cache_volume_path: "/var/cache/syncode".to_owned(), + cached_images: Vec::new(), + cached_actions: Vec::new(), + } +} + +fn hello(sequence: u64, node: String, credential: String, capacity: Capacity) -> NodeMessage { + NodeMessage { + sequence, + message_id: format!("node-message-{sequence}"), + idempotency_key: format!("node-message-{sequence}"), + body: Some(node_message::Body::Hello(Hello { + node, + credential, + capabilities: Some(capabilities()), + capacity: Some(capacity), + max_parallel: 1, + })), + } +} + +/// What a node does before it can be given anything: spend a token, then open +/// a session with the credential it got back. +async fn enrolled( + harness: &Harness, + sender: &mpsc::Sender, + inbound: &mut Streaming, +) -> TestResult<(String, String)> { + let token = harness.nodes.issue_token(Scope::Instance).await?; + sender + .send(NodeMessage { + sequence: 1, + message_id: "node-message-1".to_owned(), + idempotency_key: "node-message-1".to_owned(), + body: Some(node_message::Body::Enrol(Enrol { + token: token.secret().expose().to_owned(), + })), + }) + .await?; + let message = next(inbound).await?; + let Some(control_message::Body::Enrolled(enrolled)) = message.body else { + panic!("expected an identity, got {:?}", message.body); + }; + Ok((enrolled.node, enrolled.credential)) +} + +async fn start() -> TestResult { + let runs = Runs::restored(Forgotten::default()).await?; + let nodes = Nodes::restored(Ephemeral::default()).await?; + let listener = TcpListener::bind("127.0.0.1:0").await?; + let address = format!("http://{}", listener.local_addr()?); + let served = runs.clone(); + let served_nodes = nodes.clone(); + + tokio::spawn(async move { + let _ = Server::builder() + .add_service(GeneratedServer::new(NodeSessionServer::new( + served, + served_nodes, + CapabilityAuthority::new("test-capability-key").expect("capability key"), + ArtifactTokenAuthority::new("test-artifact-key").expect("artifact key"), + "http://127.0.0.1:1/".parse().expect("artifact URL"), + RepositoryCoordinates::new("http://127.0.0.1:1".to_owned(), String::new()) + .expect("repository coordinates"), + ))) + .serve_with_incoming(TcpListenerStream::new(listener)) + .await; + }); + + Ok(Harness { + runs, + nodes, + address, + }) +} + +async fn wait_until_offline(harness: &Harness, node: syncode_control_runs::NodeId) -> TestResult { + for _ in 0..100 { + if harness.nodes.lifecycle(node).await? == Lifecycle::Offline { + return Ok(()); + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + Err("the closed node session did not become offline".into()) +} + +async fn next(stream: &mut Streaming) -> TestResult { + Ok(stream + .next() + .await + .ok_or("the control plane closed the stream")??) +} + +#[tokio::test] +async fn node_is_handed_the_plan_the_control_plane_compiled() -> TestResult { + let harness = start().await?; + let plan = VersionedPlan::new(compile()); + let run = harness + .runs + .queue(JobId::fresh(), origin(), serde_json::to_vec(&plan)?) + .await?; + + let (sender, receiver) = mpsc::channel(8); + let mut client = NodeSessionClient::connect(harness.address.clone()).await?; + let mut inbound = client + .open(ReceiverStream::new(receiver)) + .await? + .into_inner(); + + let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; + sender + .send(NodeMessage { + sequence: 2, + message_id: "node-message-2".to_owned(), + idempotency_key: "node-message-2".to_owned(), + body: Some(node_message::Body::Hello(Hello { + node: node.clone(), + credential, + capabilities: Some(capabilities()), + capacity: Some(capacity()), + max_parallel: 1, + })), + }) + .await?; + + let welcome = next(&mut inbound).await?; + assert!(matches!( + welcome.body, + Some(control_message::Body::Welcome(_)) + )); + assert_eq!(welcome.sequence, 2, "the stream numbers what it sends"); + + let assigned = next(&mut inbound).await?; + let Some(control_message::Body::Assignment(assignment)) = assigned.body else { + panic!("expected an assignment, got {:?}", assigned.body); + }; + assert_eq!(assignment.run, run.to_string()); + assert_eq!(assigned.sequence, 3); + + let carried: VersionedPlan = serde_json::from_slice(&assignment.plan)?; + assert_eq!(carried.schema(), PlanSchemaVersion::CURRENT); + assert_eq!(carried, plan); + assert_eq!(carried.plan().job().key().as_ref(), "build"); + + let RunState::Assigned(lease) = harness.runs.state_of(run).await? else { + panic!("the run must know who holds it before the assignment leaves"); + }; + assert_eq!(lease.node().to_string(), node); + assert_eq!(lease.fence().get(), assignment.fence); + + sender + .send(progress( + &run.to_string(), + &assignment.job, + assignment.fence, + Conclusion::Success, + )) + .await?; + + let mut state = harness.runs.state_of(run).await?; + for _ in 0..50 { + if matches!(state, RunState::Finished(_)) { + break; + } + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + state = harness.runs.state_of(run).await?; + } + assert_eq!(state, RunState::Finished(Conclusion::Success)); + + Ok(()) +} + +#[tokio::test] +async fn cancellation_is_pushed_with_the_current_job_and_fence() -> TestResult { + let harness = start().await?; + let job = JobId::fresh(); + let run = harness + .runs + .queue( + job, + origin(), + serde_json::to_vec(&VersionedPlan::new(compile()))?, + ) + .await?; + let (sender, receiver) = mpsc::channel(8); + let mut client = NodeSessionClient::connect(harness.address.clone()).await?; + let mut inbound = client + .open(ReceiverStream::new(receiver)) + .await? + .into_inner(); + let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; + sender.send(hello(2, node, credential, capacity())).await?; + let _ = next(&mut inbound).await?; + let assignment_message = next(&mut inbound).await?; + let Some(control_message::Body::Assignment(assignment)) = assignment_message.body else { + return Err("expected an assignment".into()); + }; + + let dispatch = harness.runs.cancel_job(run, job).await?; + assert!(matches!( + dispatch, + syncode_control_runs::CancellationDispatch::Requested(_) + )); + let cancellation_message = + tokio::time::timeout(Duration::from_secs(1), next(&mut inbound)).await??; + let cancellation_sequence = cancellation_message.sequence; + let Some(control_message::Body::Cancel(cancellation)) = cancellation_message.body else { + return Err("expected a cancellation".into()); + }; + assert_eq!(cancellation.run, run.to_string()); + assert_eq!(cancellation.job, job.to_string()); + assert_eq!(cancellation.fence, assignment.fence); + + sender + .send(NodeMessage { + sequence: 3, + message_id: "cancel-ack".to_owned(), + idempotency_key: "cancel-ack".to_owned(), + body: Some(node_message::Body::Acknowledgement(Acknowledgement { + message_id: cancellation_message.message_id, + log_offset: 0, + acknowledged_outputs: Vec::new(), + conclusion: syncode_control_node::wire::Conclusion::Unspecified as i32, + })), + }) + .await?; + sender + .send(NodeMessage { + sequence: 4, + message_id: "heartbeat-after-cancel-ack".to_owned(), + idempotency_key: "heartbeat-after-cancel-ack".to_owned(), + body: Some(node_message::Body::Heartbeat(Heartbeat { + held: Vec::new(), + capacity: Some(capacity()), + })), + }) + .await?; + let heartbeat_acknowledgement = next(&mut inbound).await?; + assert_eq!( + heartbeat_acknowledgement.sequence, + cancellation_sequence + 1 + ); + assert!(matches!( + heartbeat_acknowledgement.body, + Some(control_message::Body::Acknowledgement(_)) + )); + + sender + .send(NodeMessage { + sequence: 5, + message_id: "cancelled-progress".to_owned(), + idempotency_key: "cancelled-progress".to_owned(), + body: Some(node_message::Body::Progress(JobProgress { + run: run.to_string(), + job: job.to_string(), + conclusion: syncode_control_node::wire::Conclusion::Cancelled as i32, + fence: assignment.fence, + outputs: Default::default(), + })), + }) + .await?; + + for _ in 0..50 { + if harness.runs.state_of(run).await? == RunState::Finished(Conclusion::Cancelled) { + return Ok(()); + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + Err("cancelled progress did not finish the run".into()) +} + +#[tokio::test] +async fn a_job_requirement_mismatch_does_not_refuse_the_node() -> TestResult { + let harness = start().await?; + let event = Event::new( + EventKind::Push, + syncode_workflow::GitReference::Branch("main".to_owned()), + Vec::new(), + ); + let triggered = trigger( + &harness.runs, + br#" +on: [push] +jobs: + build: + runs-on: [self-hosted, linux, x64] + steps: + - run: cargo test +"#, + &event, + &origin(), + &FIXTURE_ACTIONS, + ) + .await?; + let Triggered::Runs(runs) = triggered else { + return Err("workflow did not trigger".into()); + }; + let run = *runs.first().ok_or("trigger returned no run")?; + + let (sender, receiver) = mpsc::channel(8); + let mut client = NodeSessionClient::connect(harness.address.clone()).await?; + let mut inbound = client + .open(ReceiverStream::new(receiver)) + .await? + .into_inner(); + let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; + sender + .send(NodeMessage { + sequence: 2, + message_id: "typed-hello".to_owned(), + idempotency_key: "typed-hello".to_owned(), + body: Some(node_message::Body::Hello(Hello { + node, + credential, + capabilities: Some(capabilities()), + capacity: Some(capacity()), + max_parallel: 1, + })), + }) + .await?; + + assert!(matches!( + next(&mut inbound).await?.body, + Some(control_message::Body::Welcome(_)) + )); + assert!( + tokio::time::timeout(Duration::from_millis(200), inbound.next()) + .await + .is_err(), + "a job requirement mismatch closed the healthy node session" + ); + assert_eq!(harness.runs.state_of(run).await?, RunState::Queued); + + harness + .runs + .queue( + JobId::fresh(), + origin(), + serde_json::to_vec(&VersionedPlan::new(compile()))?, + ) + .await?; + let assignment = tokio::time::timeout(Duration::from_secs(1), next(&mut inbound)).await??; + assert!(matches!( + assignment.body, + Some(control_message::Body::Assignment(_)) + )); + Ok(()) +} + +#[tokio::test] +async fn stream_dispatch_does_not_let_one_organization_monopolize_capacity() -> TestResult { + let harness = start().await?; + let plan = serde_json::to_vec(&VersionedPlan::new(compile()))?; + for _ in 0..3 { + harness + .runs + .queue( + JobId::fresh(), + repository_origin("backlog/project"), + plan.clone(), + ) + .await?; + } + harness + .runs + .queue(JobId::fresh(), repository_origin("neighbor/project"), plan) + .await?; + + let (sender, receiver) = mpsc::channel(8); + let mut client = NodeSessionClient::connect(harness.address.clone()).await?; + let mut inbound = client + .open(ReceiverStream::new(receiver)) + .await? + .into_inner(); + let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; + sender + .send(NodeMessage { + sequence: 2, + message_id: "fair-hello".to_owned(), + idempotency_key: "fair-hello".to_owned(), + body: Some(node_message::Body::Hello(Hello { + node, + credential, + capabilities: Some(capabilities()), + capacity: Some(capacity()), + max_parallel: 2, + })), + }) + .await?; + let _ = next(&mut inbound).await?; + let first = next(&mut inbound).await?; + let second = next(&mut inbound).await?; + let Some(control_message::Body::Assignment(first)) = first.body else { + return Err("first message was not an assignment".into()); + }; + let Some(control_message::Body::Assignment(second)) = second.body else { + return Err("second message was not an assignment".into()); + }; + assert_eq!( + first + .origin + .ok_or("first assignment has no origin")? + .repository, + "backlog/project" + ); + assert_eq!( + second + .origin + .ok_or("second assignment has no origin")? + .repository, + "neighbor/project" + ); + Ok(()) +} + +#[tokio::test] +async fn stream_dispatch_selects_the_warmest_connected_node() -> TestResult { + let harness = start().await?; + let (cold_sender, cold_receiver) = mpsc::channel(8); + let mut cold_client = NodeSessionClient::connect(harness.address.clone()).await?; + let mut cold_inbound = cold_client + .open(ReceiverStream::new(cold_receiver)) + .await? + .into_inner(); + let (cold_node, cold_credential) = enrolled(&harness, &cold_sender, &mut cold_inbound).await?; + let mut cold_capacity = capacity(); + cold_capacity.cache_volume_present = false; + cold_capacity.cached_images.clear(); + cold_capacity.cached_actions.clear(); + cold_sender + .send(hello(2, cold_node, cold_credential, cold_capacity)) + .await?; + assert!(matches!( + next(&mut cold_inbound).await?.body, + Some(control_message::Body::Welcome(_)) + )); + + let (warm_sender, warm_receiver) = mpsc::channel(8); + let mut warm_client = NodeSessionClient::connect(harness.address.clone()).await?; + let mut warm_inbound = warm_client + .open(ReceiverStream::new(warm_receiver)) + .await? + .into_inner(); + let (warm_node, warm_credential) = enrolled(&harness, &warm_sender, &mut warm_inbound).await?; + let mut warm_capacity = capacity(); + warm_capacity.cached_images = vec!["postgres:18".to_owned()]; + warm_capacity.cached_actions = vec!["actions/checkout@v5".to_owned()]; + warm_sender + .send(hello(2, warm_node, warm_credential, warm_capacity)) + .await?; + assert!(matches!( + next(&mut warm_inbound).await?.body, + Some(control_message::Body::Welcome(_)) + )); + + harness + .runs + .queue_run( + origin(), + vec![ + QueuedJob::new( + JobId::fresh(), + "build".to_owned(), + Vec::new(), + MatrixPolicy::default(), + vec![1], + ) + .scheduled( + Priority::Normal, + Requirements::default().prefer( + ["postgres:18".to_owned()], + ["actions/checkout@v5".to_owned()], + ), + ), + ], + ) + .await?; + + let message = tokio::time::timeout(Duration::from_secs(2), next(&mut warm_inbound)).await??; + assert!(matches!( + message.body, + Some(control_message::Body::Assignment(_)) + )); + assert!( + tokio::time::timeout(Duration::from_millis(200), next(&mut cold_inbound)) + .await + .is_err() + ); + Ok(()) +} + +#[tokio::test] +async fn a_node_receives_only_its_declared_number_of_concurrent_jobs() -> TestResult { + let harness = start().await?; + let plan = serde_json::to_vec(&VersionedPlan::new(compile()))?; + harness + .runs + .queue(JobId::fresh(), origin(), plan.clone()) + .await?; + harness + .runs + .queue(JobId::fresh(), origin(), plan.clone()) + .await?; + + let (sender, receiver) = mpsc::channel(8); + let mut client = NodeSessionClient::connect(harness.address.clone()).await?; + let mut inbound = client + .open(ReceiverStream::new(receiver)) + .await? + .into_inner(); + let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; + sender + .send(NodeMessage { + sequence: 2, + message_id: "node-message-2".to_owned(), + idempotency_key: "node-message-2".to_owned(), + body: Some(node_message::Body::Hello(Hello { + node: node.clone(), + credential, + capabilities: Some(capabilities()), + capacity: Some(capacity()), + max_parallel: 2, + })), + }) + .await?; + + assert!(matches!( + next(&mut inbound).await?.body, + Some(control_message::Body::Welcome(_)) + )); + let first = next(&mut inbound).await?; + let second = next(&mut inbound).await?; + let Some(control_message::Body::Assignment(first)) = first.body else { + panic!("expected the first assignment"); + }; + let Some(control_message::Body::Assignment(second)) = second.body else { + panic!("expected the second assignment"); + }; + + harness.runs.queue(JobId::fresh(), origin(), plan).await?; + assert!( + tokio::time::timeout(Duration::from_millis(100), inbound.next()) + .await + .is_err(), + "a full node received a third concurrent job" + ); + + let completed = progress(&first.run, &first.job, first.fence, Conclusion::Success); + sender.send(completed.clone()).await?; + sender.send(completed).await?; + let replacement = next(&mut inbound).await?; + assert!(matches!( + replacement.body, + Some(control_message::Body::Assignment(_)) + )); + + sender + .send(NodeMessage { + sequence: 4, + message_id: "node-message-4".to_owned(), + idempotency_key: "node-message-4".to_owned(), + body: Some(node_message::Body::Progress(JobProgress { + run: second.run, + job: second.job, + conclusion: syncode_control_node::wire::Conclusion::Success.into(), + fence: second.fence, + outputs: Default::default(), + })), + }) + .await?; + Ok(()) +} + +#[tokio::test] +async fn an_out_of_order_node_message_closes_the_runtime_session() -> TestResult { + let harness = start().await?; + let (sender, receiver) = mpsc::channel(8); + let mut client = NodeSessionClient::connect(harness.address.clone()).await?; + let mut inbound = client + .open(ReceiverStream::new(receiver)) + .await? + .into_inner(); + let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; + sender + .send(NodeMessage { + sequence: 3, + message_id: "node-message-3".to_owned(), + idempotency_key: "node-message-3".to_owned(), + body: Some(node_message::Body::Hello(Hello { + node, + credential, + capabilities: Some(capabilities()), + capacity: Some(capacity()), + max_parallel: 1, + })), + }) + .await?; + + let error = next(&mut inbound) + .await + .expect_err("the skipped sequence must close the stream"); + assert!(error.to_string().contains("while 2 was required")); + Ok(()) +} + +#[tokio::test] +async fn work_queued_after_hello_is_pushed_without_another_heartbeat() -> TestResult { + let harness = start().await?; + let (sender, receiver) = mpsc::channel(8); + let mut client = NodeSessionClient::connect(harness.address.clone()).await?; + let mut inbound = client + .open(ReceiverStream::new(receiver)) + .await? + .into_inner(); + let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; + sender + .send(NodeMessage { + sequence: 2, + message_id: "node-message-2".to_owned(), + idempotency_key: "node-message-2".to_owned(), + body: Some(node_message::Body::Hello(Hello { + node, + credential, + capabilities: Some(capabilities()), + capacity: Some(capacity()), + max_parallel: 1, + })), + }) + .await?; + assert!(matches!( + next(&mut inbound).await?.body, + Some(control_message::Body::Welcome(_)) + )); + + harness + .runs + .queue( + JobId::fresh(), + origin(), + serde_json::to_vec(&VersionedPlan::new(compile()))?, + ) + .await?; + let pushed = tokio::time::timeout(Duration::from_secs(1), next(&mut inbound)).await??; + assert!(matches!( + pushed.body, + Some(control_message::Body::Assignment(_)) + )); + Ok(()) +} + +/// A node that keeps beating is handed its next credential before the one it +/// holds runs out, and the new one is what opens the session after that. Without +/// this a node works for exactly one credential term and is then locked out of +/// the control plane for good, with no way back but a fresh enrolment token. +#[tokio::test] +async fn a_credential_near_expiry_is_replaced_over_the_open_session() -> TestResult { + let harness = start().await?; + let (sender, receiver) = mpsc::channel(8); + let mut client = NodeSessionClient::connect(harness.address.clone()).await?; + let mut inbound = client + .open(ReceiverStream::new(receiver)) + .await? + .into_inner(); + + let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; + sender + .send(NodeMessage { + sequence: 2, + message_id: "node-message-2".to_owned(), + idempotency_key: "node-message-2".to_owned(), + body: Some(node_message::Body::Hello(Hello { + node: node.clone(), + credential: credential.clone(), + capabilities: Some(capabilities()), + capacity: Some(capacity()), + max_parallel: 1, + })), + }) + .await?; + + let welcome = next(&mut inbound).await?; + assert!( + matches!(welcome.body, Some(control_message::Body::Welcome(_))), + "a fresh credential is not renewed, got {:?}", + welcome.body + ); + + // Age the credential by issuing it as though the term had nearly run out. + // Sixty seconds of life left is inside the renewal margin, so the next thing + // the node says has to be answered with a replacement. + let issued = SystemTime::now() - CREDENTIAL_TERM + Duration::from_secs(60); + let ageing = harness.nodes.rotate(node.parse()?, issued).await?; + sender + .send(NodeMessage { + sequence: 3, + message_id: "node-message-3".to_owned(), + idempotency_key: "node-message-3".to_owned(), + body: Some(node_message::Body::Heartbeat(Heartbeat { + held: Vec::new(), + capacity: Some(capacity()), + })), + }) + .await?; + + let answer = next(&mut inbound).await?; + let Some(control_message::Body::Rotated(rotated)) = answer.body else { + panic!("expected a replacement credential, got {:?}", answer.body); + }; + assert_ne!( + rotated.credential, + ageing.secret().expose(), + "a rotation that hands back the same credential renews nothing" + ); + assert_ne!(rotated.credential, credential); + let acknowledged = next(&mut inbound).await?; + assert!(matches!( + acknowledged.body, + Some(control_message::Body::Acknowledgement(_)) + )); + + sender + .send(NodeMessage { + sequence: 4, + message_id: "node-message-4".to_owned(), + idempotency_key: "node-message-4".to_owned(), + body: Some(node_message::Body::Heartbeat(Heartbeat { + held: Vec::new(), + capacity: Some(capacity()), + })), + }) + .await?; + let repeated = next(&mut inbound).await?; + let Some(control_message::Body::Rotated(repeated)) = repeated.body else { + panic!("expected the unconfirmed credential again"); + }; + assert_eq!(repeated.credential, rotated.credential); + + drop(sender); + drop(inbound); + wait_until_offline(&harness, node.parse()?).await?; + + // The point of the exercise: the credential the node was pushed is the one + // that gets it back in, and the one it replaced does not. + let (fresh_sender, fresh_receiver) = mpsc::channel(8); + let mut reopened = client + .open(ReceiverStream::new(fresh_receiver)) + .await? + .into_inner(); + fresh_sender + .send(NodeMessage { + sequence: 1, + message_id: "node-message-1".to_owned(), + idempotency_key: "node-message-1".to_owned(), + body: Some(node_message::Body::Hello(Hello { + node: node.clone(), + credential: rotated.credential, + capabilities: Some(capabilities()), + capacity: Some(capacity()), + max_parallel: 1, + })), + }) + .await?; + assert!(matches!( + next(&mut reopened).await?.body, + Some(control_message::Body::Welcome(_)) + )); + + let (stale_sender, stale_receiver) = mpsc::channel(8); + let mut refused = client + .open(ReceiverStream::new(stale_receiver)) + .await? + .into_inner(); + stale_sender + .send(NodeMessage { + sequence: 1, + message_id: "node-message-1".to_owned(), + idempotency_key: "node-message-1".to_owned(), + body: Some(node_message::Body::Hello(Hello { + node, + credential, + capabilities: Some(capabilities()), + capacity: Some(capacity()), + max_parallel: 1, + })), + }) + .await?; + let error = next(&mut refused) + .await + .expect_err("the credential that was replaced must stop working"); + assert!( + error.to_string().contains("not current"), + "unexpected error: {error}" + ); + + Ok(()) +} + +/// An identity handed out to something that never arrived has to settle, or the +/// sweep reports it as newly gone every fifteen seconds forever and the log +/// stops being worth reading. +#[tokio::test] +async fn a_node_that_enrolled_and_never_spoke_is_reported_gone_once() -> TestResult { + let harness = start().await?; + let token = harness.nodes.issue_token(Scope::Instance).await?; + let now = SystemTime::now(); + let (node, _) = harness.nodes.enrol(token.secret().expose(), now).await?; + + let deadline = Duration::from_secs(90); + let late = now + Duration::from_secs(600); + assert_eq!( + harness.nodes.sweep_silent(deadline, late).await?, + vec![node] + ); + assert!( + harness.nodes.sweep_silent(deadline, late).await?.is_empty(), + "a node already marked offline is not gone again" + ); + + Ok(()) +} + +fn progress(run: &str, job: &str, fence: u64, conclusion: Conclusion) -> NodeMessage { + let conclusion = match conclusion { + Conclusion::Success => syncode_control_node::wire::Conclusion::Success, + Conclusion::Failure => syncode_control_node::wire::Conclusion::Failure, + Conclusion::Cancelled => syncode_control_node::wire::Conclusion::Cancelled, + Conclusion::Skipped => syncode_control_node::wire::Conclusion::Skipped, + }; + NodeMessage { + sequence: 3, + message_id: "node-message-3".to_owned(), + idempotency_key: "node-message-3".to_owned(), + body: Some(node_message::Body::Progress(JobProgress { + run: run.to_owned(), + job: job.to_owned(), + conclusion: conclusion.into(), + fence, + outputs: Default::default(), + })), + } +} + +#[tokio::test] +async fn report_stating_a_grant_that_is_not_current_is_refused() -> TestResult { + let harness = start().await?; + let plan = VersionedPlan::new(compile()); + let run = harness + .runs + .queue(JobId::fresh(), origin(), serde_json::to_vec(&plan)?) + .await?; + + let holder = syncode_control_runs::NodeId::fresh(); + let assignment = harness + .runs + .take_next(holder) + .await? + .ok_or("nothing was queued")?; + + let error = harness + .runs + .finished( + run, + holder, + Fence::from(assignment.fence().get() + 1), + Conclusion::Success, + ) + .await + .expect_err("a grant nobody was given must not conclude a run"); + + assert!( + error.to_string().contains("not the current grant"), + "unexpected error: {error}" + ); + assert!(matches!( + harness.runs.state_of(run).await?, + RunState::Assigned(_) + )); + + Ok(()) +} + +#[tokio::test] +async fn a_session_without_a_credential_is_refused() -> TestResult { + let harness = start().await?; + let (sender, receiver) = mpsc::channel(8); + let mut client = NodeSessionClient::connect(harness.address.clone()).await?; + let mut inbound = client + .open(ReceiverStream::new(receiver)) + .await? + .into_inner(); + + let (node, _) = enrolled(&harness, &sender, &mut inbound).await?; + sender + .send(NodeMessage { + sequence: 2, + message_id: "node-message-2".to_owned(), + idempotency_key: "node-message-2".to_owned(), + body: Some(node_message::Body::Hello(Hello { + node, + credential: "not the one it was given".to_owned(), + capabilities: Some(capabilities()), + capacity: Some(capacity()), + max_parallel: 1, + })), + }) + .await?; + + let error = next(&mut inbound) + .await + .expect_err("a wrong credential must end the session"); + assert!( + error.to_string().contains("not current"), + "unexpected error: {error}" + ); + + Ok(()) +} + +#[tokio::test] +async fn a_node_that_does_not_say_how_much_room_it_has_is_refused() -> TestResult { + let harness = start().await?; + let (sender, receiver) = mpsc::channel(8); + let mut client = NodeSessionClient::connect(harness.address.clone()).await?; + let mut inbound = client + .open(ReceiverStream::new(receiver)) + .await? + .into_inner(); + + let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; + sender + .send(NodeMessage { + sequence: 2, + message_id: "node-message-2".to_owned(), + idempotency_key: "node-message-2".to_owned(), + body: Some(node_message::Body::Hello(Hello { + node, + credential, + capabilities: Some(capabilities()), + capacity: None, + max_parallel: 1, + })), + }) + .await?; + + let error = next(&mut inbound) + .await + .expect_err("a node without capacity must not open a session"); + assert!( + error.to_string().contains("how much room"), + "unexpected error: {error}" + ); + + Ok(()) +} + +#[tokio::test] +async fn a_failed_capacity_refresh_pauses_work_without_closing_the_session() -> TestResult { + let harness = start().await?; + let (sender, receiver) = mpsc::channel(8); + let mut client = NodeSessionClient::connect(harness.address.clone()).await?; + let mut inbound = client + .open(ReceiverStream::new(receiver)) + .await? + .into_inner(); + let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; + sender + .send(NodeMessage { + sequence: 2, + message_id: "node-message-2".to_owned(), + idempotency_key: "node-message-2".to_owned(), + body: Some(node_message::Body::Hello(Hello { + node: node.clone(), + credential, + capabilities: Some(capabilities()), + capacity: Some(capacity()), + max_parallel: 1, + })), + }) + .await?; + assert!(matches!( + next(&mut inbound).await?.body, + Some(control_message::Body::Welcome(_)) + )); + + sender + .send(NodeMessage { + sequence: 3, + message_id: "node-message-3".to_owned(), + idempotency_key: "node-message-3".to_owned(), + body: Some(node_message::Body::Heartbeat(Heartbeat { + held: Vec::new(), + capacity: None, + })), + }) + .await?; + let node_id = node.parse()?; + for _ in 0..50 { + if !harness.nodes.employable(node_id).await? { + break; + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + assert!(!harness.nodes.employable(node_id).await?); + assert!(matches!( + next(&mut inbound).await?.body, + Some(control_message::Body::Acknowledgement(_)) + )); + harness + .runs + .queue( + JobId::fresh(), + origin(), + serde_json::to_vec(&VersionedPlan::new(compile()))?, + ) + .await?; + + assert!( + tokio::time::timeout(Duration::from_millis(100), next(&mut inbound)) + .await + .is_err(), + "an unmeasured node must stay connected without receiving work" + ); + + sender + .send(NodeMessage { + sequence: 4, + message_id: "node-message-4".to_owned(), + idempotency_key: "node-message-4".to_owned(), + body: Some(node_message::Body::Heartbeat(Heartbeat { + held: Vec::new(), + capacity: Some(capacity()), + })), + }) + .await?; + let answer = next(&mut inbound).await?; + assert!( + matches!(answer.body, Some(control_message::Body::Assignment(_))), + "a recovered capacity measurement must resume work" + ); + Ok(()) +} + +#[tokio::test] +async fn a_node_short_of_room_is_told_why_instead_of_being_given_work() -> TestResult { + let harness = start().await?; + let plan = VersionedPlan::new(compile()); + harness + .runs + .queue(JobId::fresh(), origin(), serde_json::to_vec(&plan)?) + .await?; + + let (sender, receiver) = mpsc::channel(8); + let mut client = NodeSessionClient::connect(harness.address.clone()).await?; + let mut inbound = client + .open(ReceiverStream::new(receiver)) + .await? + .into_inner(); + + let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; + let cramped = Capacity { + build_volume_free_bytes: 1024, + ..capacity() + }; + sender + .send(NodeMessage { + sequence: 2, + message_id: "node-message-2".to_owned(), + idempotency_key: "node-message-2".to_owned(), + body: Some(node_message::Body::Hello(Hello { + node, + credential, + capabilities: Some(capabilities()), + capacity: Some(cramped), + max_parallel: 1, + })), + }) + .await?; + + let welcome = next(&mut inbound).await?; + assert!(matches!( + welcome.body, + Some(control_message::Body::Welcome(_)) + )); + + let answer = next(&mut inbound).await?; + let Some(control_message::Body::Refused(refused)) = answer.body else { + panic!("expected a stated refusal, got {:?}", answer.body); + }; + assert!( + refused.reason.contains("below the"), + "unexpected reason: {}", + refused.reason + ); + + Ok(()) +} + +#[tokio::test] +async fn an_applied_message_is_deduplicated_after_reconnect() -> TestResult { + let harness = start().await?; + let (first_sender, first_receiver) = mpsc::channel(8); + let mut client = NodeSessionClient::connect(harness.address.clone()).await?; + let mut first = client + .open(ReceiverStream::new(first_receiver)) + .await? + .into_inner(); + let (node, credential) = enrolled(&harness, &first_sender, &mut first).await?; + first_sender + .send(NodeMessage { + sequence: 2, + message_id: "first-hello".to_owned(), + idempotency_key: "first-hello".to_owned(), + body: Some(node_message::Body::Hello(Hello { + node: node.clone(), + credential: credential.clone(), + capabilities: Some(capabilities()), + capacity: Some(capacity()), + max_parallel: 1, + })), + }) + .await?; + let _ = next(&mut first).await?; + let repeated = NodeMessage { + sequence: 3, + message_id: "persistent-heartbeat".to_owned(), + idempotency_key: "persistent-heartbeat".to_owned(), + body: Some(node_message::Body::Heartbeat(Heartbeat { + held: Vec::new(), + capacity: None, + })), + }; + first_sender.send(repeated.clone()).await?; + let node_id = node.parse()?; + for _ in 0..50 { + if !harness.nodes.employable(node_id).await? { + break; + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + assert!(!harness.nodes.employable(node_id).await?); + + drop(first_sender); + drop(first); + wait_until_offline(&harness, node_id).await?; + + let (second_sender, second_receiver) = mpsc::channel(8); + let mut second_client = NodeSessionClient::connect(harness.address.clone()).await?; + let mut second = second_client + .open(ReceiverStream::new(second_receiver)) + .await? + .into_inner(); + second_sender + .send(NodeMessage { + sequence: 1, + message_id: "second-hello".to_owned(), + idempotency_key: "second-hello".to_owned(), + body: Some(node_message::Body::Hello(Hello { + node, + credential, + capabilities: Some(capabilities()), + capacity: Some(capacity()), + max_parallel: 1, + })), + }) + .await?; + let _ = next(&mut second).await?; + assert!(harness.nodes.employable(node_id).await?); + + second_sender + .send(NodeMessage { + sequence: 2, + ..repeated + }) + .await?; + tokio::time::sleep(Duration::from_millis(50)).await; + assert!( + harness.nodes.employable(node_id).await?, + "the duplicate must not clear capacity again" + ); + + second_sender + .send(NodeMessage { + sequence: 3, + message_id: "new-heartbeat".to_owned(), + idempotency_key: "new-heartbeat".to_owned(), + body: Some(node_message::Body::Heartbeat(Heartbeat { + held: Vec::new(), + capacity: None, + })), + }) + .await?; + for _ in 0..50 { + if !harness.nodes.employable(node_id).await? { + break; + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + assert!(!harness.nodes.employable(node_id).await?); + Ok(()) +} + +#[tokio::test] +async fn an_unacknowledged_assignment_is_retried_on_the_open_session() -> TestResult { + let harness = start().await?; + harness + .runs + .queue( + JobId::fresh(), + origin(), + serde_json::to_vec(&VersionedPlan::new(compile()))?, + ) + .await?; + + let (sender, receiver) = mpsc::channel(8); + let mut client = NodeSessionClient::connect(harness.address.clone()).await?; + let mut inbound = client + .open(ReceiverStream::new(receiver)) + .await? + .into_inner(); + let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; + sender + .send(NodeMessage { + sequence: 2, + message_id: "hello".to_owned(), + idempotency_key: "hello".to_owned(), + body: Some(node_message::Body::Hello(Hello { + node: node.clone(), + credential, + capabilities: Some(capabilities()), + capacity: Some(capacity()), + max_parallel: 1, + })), + }) + .await?; + let _ = next(&mut inbound).await?; + let assignment = next(&mut inbound).await?; + assert!(matches!( + assignment.body, + Some(control_message::Body::Assignment(_)) + )); + + let retried = tokio::time::timeout(Duration::from_secs(3), next(&mut inbound)).await??; + assert_eq!(retried, assignment); + + sender + .send(NodeMessage { + sequence: 3, + message_id: "assignment-ack".to_owned(), + idempotency_key: "assignment-ack".to_owned(), + body: Some(node_message::Body::Acknowledgement(Acknowledgement { + message_id: assignment.message_id.clone(), + log_offset: 0, + acknowledged_outputs: Vec::new(), + conclusion: 0, + })), + }) + .await?; + let node = node.parse()?; + for _ in 0..50 { + if harness.nodes.outbound_pending(node).await?.is_empty() { + break; + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + assert!(harness.nodes.outbound_pending(node).await?.is_empty()); + + let acknowledgement = Acknowledgement { + message_id: assignment.message_id, + log_offset: 0, + acknowledged_outputs: Vec::new(), + conclusion: 0, + }; + sender + .send(NodeMessage { + sequence: 4, + message_id: "retried-assignment-ack".to_owned(), + idempotency_key: "assignment-ack".to_owned(), + body: Some(node_message::Body::Acknowledgement(acknowledgement)), + }) + .await?; + sender + .send(NodeMessage { + sequence: 5, + message_id: "heartbeat-after-retried-ack".to_owned(), + idempotency_key: "heartbeat-after-retried-ack".to_owned(), + body: Some(node_message::Body::Heartbeat(Heartbeat { + held: Vec::new(), + capacity: Some(capacity()), + })), + }) + .await?; + assert!(matches!( + next(&mut inbound).await?.body, + Some(control_message::Body::Acknowledgement(_)) + )); + Ok(()) +} + +#[tokio::test] +async fn reconnect_does_not_restore_capacity_while_a_lease_is_held() -> TestResult { + let harness = start().await?; + let plan = serde_json::to_vec(&VersionedPlan::new(compile()))?; + let first_run = harness + .runs + .queue(JobId::fresh(), origin(), plan.clone()) + .await?; + let second_run = harness.runs.queue(JobId::fresh(), origin(), plan).await?; + let (sender, receiver) = mpsc::channel(8); + let mut client = NodeSessionClient::connect(harness.address.clone()).await?; + let mut inbound = client + .open(ReceiverStream::new(receiver)) + .await? + .into_inner(); + let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; + sender + .send(hello(2, node.clone(), credential.clone(), capacity())) + .await?; + let _ = next(&mut inbound).await?; + let assigned = next(&mut inbound).await?; + let Some(control_message::Body::Assignment(assignment)) = assigned.body.clone() else { + return Err("expected the first assignment".into()); + }; + assert_eq!(assignment.run, first_run.to_string()); + sender + .send(NodeMessage { + sequence: 3, + message_id: "assignment-ack".to_owned(), + idempotency_key: "assignment-ack".to_owned(), + body: Some(node_message::Body::Acknowledgement(Acknowledgement { + message_id: assigned.message_id, + log_offset: 0, + acknowledged_outputs: Vec::new(), + conclusion: 0, + })), + }) + .await?; + let node_id = node.parse()?; + for _ in 0..50 { + if harness.nodes.outbound_pending(node_id).await?.is_empty() { + break; + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + drop(sender); + drop(inbound); + wait_until_offline(&harness, node_id).await?; + + let (sender, receiver) = mpsc::channel(8); + let mut reopened = client + .open(ReceiverStream::new(receiver)) + .await? + .into_inner(); + sender.send(hello(1, node, credential, capacity())).await?; + let _ = next(&mut reopened).await?; + assert!( + tokio::time::timeout(Duration::from_millis(200), next(&mut reopened)) + .await + .is_err(), + "reconnect restored a slot still occupied by the first lease" + ); + + harness + .runs + .finished_job( + first_run, + assignment.job.parse()?, + node_id, + Fence::from(assignment.fence), + Conclusion::Success, + ) + .await?; + let next_assignment = + tokio::time::timeout(Duration::from_secs(1), next(&mut reopened)).await??; + let Some(control_message::Body::Assignment(assignment)) = next_assignment.body else { + return Err("expected the second assignment".into()); + }; + assert_eq!(assignment.run, second_run.to_string()); + Ok(()) +} + +#[tokio::test] +async fn a_silent_open_session_is_refused_after_its_lease_expires() -> TestResult { + let harness = start().await?; + let plan = serde_json::to_vec(&VersionedPlan::new(compile()))?; + let run = harness.runs.queue(JobId::fresh(), origin(), plan).await?; + let (sender, receiver) = mpsc::channel(8); + let mut client = NodeSessionClient::connect(harness.address.clone()).await?; + let mut inbound = client + .open(ReceiverStream::new(receiver)) + .await? + .into_inner(); + let (node, credential) = enrolled(&harness, &sender, &mut inbound).await?; + sender + .send(hello(2, node.clone(), credential, capacity())) + .await?; + let _ = next(&mut inbound).await?; + let assignment = next(&mut inbound).await?; + assert!(matches!( + assignment.body, + Some(control_message::Body::Assignment(_)) + )); + + let node_id = node.parse()?; + let late = SystemTime::now() + Duration::from_secs(600); + assert_eq!( + harness + .nodes + .sweep_silent(Duration::from_secs(90), late) + .await?, + vec![node_id] + ); + assert_eq!(harness.nodes.lifecycle(node_id).await?, Lifecycle::Offline); + assert_eq!(harness.runs.reclaim_expired(late).await?, vec![run]); + + let refused = tokio::time::timeout(Duration::from_secs(1), next(&mut inbound)).await??; + assert!(matches!( + refused.body, + Some(control_message::Body::Refused(_)) + )); + assert_eq!(harness.runs.state_of(run).await?, RunState::Queued); + Ok(()) +} diff --git a/tests/webhook.rs b/tests/webhook.rs --- a/tests/webhook.rs +++ b/tests/webhook.rs @@ -1,759 +1,749 @@ -#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)] - -#[path = "support/actions.rs"] -mod actions; -#[path = "support/repository.rs"] -mod repository; - -use std::error::Error; -use std::sync::Arc; -use std::time::Duration; - -use base64::Engine; -use base64::engine::general_purpose::STANDARD; -use hmac::{Hmac, KeyInit, Mac}; -use sha2::Sha256; -use syncode_control::repository::RepositoryContents; -use syncode_control::repository_grpc::NativeRepositoryContents; -use syncode_control::repository_sources::RepositorySources; -use syncode_control::webhook::{Intake, router}; -use syncode_control_node::identity_wire::identity_server::{Identity, IdentityServer}; -use syncode_control_node::identity_wire::{ - CheckCapabilityRequest, CheckCapabilityResponse, GetRepositoryCoordinatesRequest, - GetRepositoryCoordinatesResponse, IssueWorkflowRepositoryTokenRequest, - IssueWorkflowRepositoryTokenResponse, ResolveRepositoryRequest, ResolveRepositoryResponse, - ValidateSessionRequest, ValidateSessionResponse, -}; -use syncode_control_node::wire::{ - Capabilities, Capacity, ControlMessage, Enrol, Hello, NodeMessage, control_message, - node_message, -}; -use syncode_control_node::{ - ArtifactTokenAuthority, CapabilityAuthority, GeneratedServer, NodeSessionClient, - NodeSessionServer, ProjectionClient, -}; -use syncode_control_nodes::{Ephemeral, Nodes, Scope}; -use syncode_control_runs::{Fence, Forgotten, NodeId, Runs}; -use syncode_workflow::VersionedPlan; -use syncode_workflow_github_actions::expression::ExpressionProgram; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::TcpListener; -use tokio::sync::mpsc; -use tokio_stream::StreamExt; -use tokio_stream::wrappers::{ReceiverStream, TcpListenerStream}; -use tonic::transport::Server; -use tonic::{Request, Response, Status, Streaming}; -use url::Url; - -use actions::FIXTURE_ACTIONS; - -type TestResult = Result>; - -struct ProjectionIdentity; - -#[tonic::async_trait] -impl Identity for ProjectionIdentity { - async fn issue_workflow_repository_token( - &self, - _request: Request, - ) -> Result, Status> { - Err(Status::unimplemented("issue_workflow_repository_token")) - } - - async fn validate_session( - &self, - _request: Request, - ) -> Result, Status> { - Err(Status::unimplemented("validate_session")) - } - - async fn check_capability( - &self, - _request: Request, - ) -> Result, Status> { - Err(Status::unimplemented("check_capability")) - } - - async fn resolve_repository( - &self, - _request: Request, - ) -> Result, Status> { - Err(Status::unimplemented("resolve_repository")) - } - - async fn get_repository_coordinates( - &self, - _request: Request, - ) -> Result, Status> { - Ok(Response::new(GetRepositoryCoordinatesResponse { - owner: "syncode".to_owned(), - name: "fixture".to_owned(), - })) - } -} - -const SECRET: &str = "the secret the hook was configured with"; -const COMMIT: &str = "9f2c1e4a7b3d5f6081a2c3d4e5f60718293a4b5c"; - -const WORKFLOW: &str = r#" -name: CI -on: - push: - branches: [main] - tags: ["v*"] - pull_request: - branches: [main] - paths: - - "src/**" - workflow_dispatch: - schedule: - - cron: "0 6 * * *" -jobs: - build: - runs-on: [self-hosted, linux] - steps: - - run: echo "the run came from a push" -"#; - -const OTHER_MANUAL_WORKFLOW: &str = r#" -name: Other manual workflow -on: - push: - workflow_dispatch: -jobs: - other: - runs-on: [self-hosted, linux] - steps: - - run: echo "the other workflow must not run" -"#; - -/// A forge that serves one workflow directory and has nothing in the other. -async fn forge() -> TestResult { - let listener = TcpListener::bind("127.0.0.1:0").await?; - let base = Url::parse(&format!("http://{}/", listener.local_addr()?))?; - let listing = format!( - r#"[{{"path":".gitea/workflows/ci.yml","type":"file","content":"{}"}},{{"path":".gitea/workflows/other.yml","type":"file","content":"{}"}}]"#, - STANDARD.encode(WORKFLOW), - STANDARD.encode(OTHER_MANUAL_WORKFLOW) - ); - - tokio::spawn(async move { - // Answers by what was asked for rather than in a fixed order: a push - // never asks for a file list, a pull request does. - let files = r#"[{"filename":"src/main.rs"}]"#.to_owned(); - loop { - let Ok((mut stream, _)) = listener.accept().await else { - return; - }; - let mut buffer = vec![0_u8; 4096]; - let read = stream.read(&mut buffer).await.unwrap_or(0); - let request = String::from_utf8_lossy(&buffer[..read]).to_string(); - let asked = request.lines().next().unwrap_or_default().to_owned(); - - let (status, body) = if asked.contains("/pulls/") { - (200, files.clone()) - } else if asked.contains(".gitea%2Fworkflows") || asked.contains(".gitea/workflows") { - (200, listing.clone()) - } else { - (404, String::from("{}")) - }; - - let response = format!( - "HTTP/1.1 {status} X\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}", - body.len() - ); - let _ = stream.write_all(response.as_bytes()).await; - let _ = stream.shutdown().await; - } - }); - - Ok(base) -} - -struct Harness { - runs: Runs, - nodes: Nodes, - session: String, - events: String, -} - -/// Both ends of the control plane over one set of registries, exactly as the -/// binary wires them. -async fn start() -> TestResult { - start_mode(false).await -} - -async fn start_mode(shadow: bool) -> TestResult { - let runs = Runs::restored(Forgotten::default()).await?; - let nodes = Nodes::restored(Ephemeral::default()).await?; - let projection = projection_client().await?; - - let sessions = TcpListener::bind("127.0.0.1:0").await?; - let session = format!("http://{}", sessions.local_addr()?); - let served = (runs.clone(), nodes.clone()); - tokio::spawn(async move { - let session = if shadow { - NodeSessionServer::shadow( - served.0, - served.1, - capability_authority(), - artifact_authority(), - "http://127.0.0.1:1/".parse().expect("artifact URL"), - projection, - ) - } else { - NodeSessionServer::new( - served.0, - served.1, - capability_authority(), - artifact_authority(), - "http://127.0.0.1:1/".parse().expect("artifact URL"), - projection, - ) - }; - let _ = Server::builder() - .add_service(GeneratedServer::new(session)) - .serve_with_incoming(TcpListenerStream::new(sessions)) - .await; - }); - - let repository_listener = TcpListener::bind("127.0.0.1:0").await?; - let repository_endpoint = format!("http://{}", repository_listener.local_addr()?); - tokio::spawn(async move { - let _ = Server::builder() - .add_service(repository::service()) - .serve_with_incoming(TcpListenerStream::new(repository_listener)) - .await; - }); - let native = NativeRepositoryContents::connect(repository_endpoint).await?; - - let deliveries = TcpListener::bind("127.0.0.1:0").await?; - let events = format!("http://{}", deliveries.local_addr()?); - let intake = Arc::new(Intake::new( - RepositorySources::new( - native, - RepositoryContents::new(forge().await?, "a repository token".to_owned()), - ), - runs.clone(), - FIXTURE_ACTIONS, - SECRET.to_owned(), - )); - tokio::spawn(async move { - let _ = axum::serve(deliveries, router(intake)).await; - }); - - Ok(Harness { - runs, - nodes, - session, - events, - }) -} - -fn capability_authority() -> CapabilityAuthority { - CapabilityAuthority::new("test-capability-key").expect("capability key") -} - -fn artifact_authority() -> ArtifactTokenAuthority { - ArtifactTokenAuthority::new("test-artifact-key").expect("artifact key") -} - -async fn projection_client() -> TestResult { - let listener = TcpListener::bind("127.0.0.1:0").await?; - let base = Url::parse(&format!("http://{}/", listener.local_addr()?))?; - tokio::spawn(async move { - let app = axum::Router::new().fallback(|| async { axum::http::StatusCode::NO_CONTENT }); - if let Err(error) = axum::serve(listener, app).await { - eprintln!("projection fixture failed: {error}"); - } - }); - let identity_listener = TcpListener::bind("127.0.0.1:0").await?; - let identity_endpoint = format!("http://{}", identity_listener.local_addr()?); - tokio::spawn(async move { - let _ = Server::builder() - .add_service(IdentityServer::new(ProjectionIdentity)) - .serve_with_incoming(TcpListenerStream::new(identity_listener)) - .await; - }); - Ok(ProjectionClient::new( - base, - String::new(), - identity_endpoint, - String::new(), - )?) -} - -async fn open_node( - harness: &Harness, -) -> TestResult<(mpsc::Sender, Streaming)> { - let (sender, receiver) = mpsc::channel(8); - let mut client = NodeSessionClient::connect(harness.session.clone()).await?; - let mut inbound = client - .open(ReceiverStream::new(receiver)) - .await? - .into_inner(); - let token = harness.nodes.issue_token(Scope::Instance).await?; - sender - .send(NodeMessage { - sequence: 1, - message_id: "node-message-1".to_owned(), - idempotency_key: "node-message-1".to_owned(), - body: Some(node_message::Body::Enrol(Enrol { - token: token.secret().expose().to_owned(), - })), - }) - .await?; - let message = next(&mut inbound).await?; - let Some(control_message::Body::Enrolled(enrolled)) = message.body else { - return Err("expected an enrolled node".into()); - }; - sender - .send(NodeMessage { - sequence: 2, - message_id: "node-message-2".to_owned(), - idempotency_key: "node-message-2".to_owned(), - body: Some(node_message::Body::Hello(Hello { - node: enrolled.node, - credential: enrolled.credential, - capabilities: Some(Capabilities { - architecture: "arm64".to_owned(), - operating_system: "linux".to_owned(), - container_runtime: "docker".to_owned(), - container_runtime_version: "28.6.1".to_owned(), - cores: 2, - memory_bytes: 8 * 1024 * 1024 * 1024, - labels: vec!["self-hosted".to_owned(), "linux".to_owned()], - }), - capacity: Some(Capacity { - build_volume_free_bytes: 60 * 1024 * 1024 * 1024, - layer_store_bytes: 12 * 1024 * 1024 * 1024, - cache_volume_present: true, - cache_volume_total_bytes: 100, - cache_volume_used_bytes: 40, - cache_volume_path: "/var/cache/syncode".to_owned(), - cached_images: Vec::new(), - cached_actions: Vec::new(), - }), - max_parallel: 1, - })), - }) - .await?; - let welcome = next(&mut inbound).await?; - if !matches!(welcome.body, Some(control_message::Body::Welcome(_))) { - return Err("expected a welcome".into()); - } - Ok((sender, inbound)) -} - -fn push_body() -> String { - format!( - r#"{{"message_id":"018f47e2-b2c4-7f19-8a6d-13ef76c89211","protocol_version":"1.0","schema_version":1,"source_node_id":"018f47e2-b2c4-7f19-8a6d-13ef76c89212","repository_id":"{}","message_type":"repository.ref.updated","sequence":2,"term":1,"payload":{{"request":{{"principal_id":"018f47e2-b2c4-7f19-8a6d-13ef76c89213"}},"changes":[{{"name_hex":"726566732f68656164732f6d61696e","old":{{"kind":"object","object_id":"1111111111111111111111111111111111111111"}},"new":{{"kind":"object","object_id":"{COMMIT}"}}}}]}}}}"#, - repository::REPOSITORY - ) -} - -fn tag_body() -> String { - format!( - r#"{{"message_id":"018f47e2-b2c4-7f19-8a6d-13ef76c89211","protocol_version":"1.0","schema_version":1,"source_node_id":"018f47e2-b2c4-7f19-8a6d-13ef76c89212","repository_id":"{}","message_type":"repository.ref.updated","sequence":2,"term":1,"payload":{{"request":{{"principal_id":"018f47e2-b2c4-7f19-8a6d-13ef76c89213"}},"changes":[{{"name_hex":"726566732f746167732f76302e352e30","old":null,"new":{{"kind":"object","object_id":"{}"}}}}]}}}}"#, - repository::REPOSITORY, - repository::TAG_OBJECT - ) -} - -fn legacy_push_body() -> String { - format!( - r#"{{"ref":"refs/heads/main","after":"{COMMIT}","repository":{{"full_name":"syncode/demo"}},"commits":[{{"modified":["src/main.rs"]}}]}}"# - ) -} - -fn signature(body: &str) -> String { - let mut mac = Hmac::::new_from_slice(SECRET.as_bytes()).expect("key"); - mac.update(body.as_bytes()); - mac.finalize() - .into_bytes() - .iter() - .map(|byte| format!("{byte:02x}")) - .collect() -} - -async fn deliver(harness: &Harness, event: &str, body: &str, signed: bool) -> TestResult { - let signature = if signed { - signature(body) - } else { - signature("something else entirely") - }; - let response = reqwest::Client::new() - .post(format!("{}/events", harness.events)) - .header("x-syncode-event", event) - .header("x-syncode-signature", signature) - .header("x-syncode-delivery", format!("{event}-delivery")) - .header( - "x-syncode-workflow", - if event == "repository.ref.updated" { - ".gitea/workflows/ci.yml" - } else { - "" - }, - ) - .body(body.to_owned()) - .send() - .await?; - Ok(response.status().as_u16()) -} - -async fn deliver_native_pull_request(harness: &Harness, body: &str) -> TestResult { - let response = reqwest::Client::new() - .post(format!("{}/events", harness.events)) - .header("x-syncode-event", "pull_request") - .header("x-syncode-signature", signature(body)) - .header("x-syncode-delivery", "native-pull-request-delivery") - .header("x-syncode-repository-id", repository::REPOSITORY) - .header( - "x-syncode-before", - "1111111111111111111111111111111111111111", - ) - .header("x-syncode-after", COMMIT) - .body(body.to_owned()) - .send() - .await?; - Ok(response.status().as_u16()) -} - -async fn next(stream: &mut Streaming) -> TestResult { - Ok(stream - .next() - .await - .ok_or("the control plane closed the stream")??) -} - -#[tokio::test] -async fn a_push_becomes_a_plan_the_node_is_handed() -> TestResult { - let harness = start().await?; - - let body = push_body(); - assert_eq!( - deliver(&harness, "repository.ref.updated", &body, true).await?, - 202 - ); - - let (sender, receiver) = mpsc::channel(8); - let mut client = NodeSessionClient::connect(harness.session.clone()).await?; - let mut inbound = client - .open(ReceiverStream::new(receiver)) - .await? - .into_inner(); - - let token = harness.nodes.issue_token(Scope::Instance).await?; - sender - .send(NodeMessage { - sequence: 1, - message_id: "node-message-1".to_owned(), - idempotency_key: "node-message-1".to_owned(), - body: Some(node_message::Body::Enrol(Enrol { - token: token.secret().expose().to_owned(), - })), - }) - .await?; - let message = next(&mut inbound).await?; - let Some(control_message::Body::Enrolled(enrolled)) = message.body else { - panic!("expected an identity, got {:?}", message.body); - }; - let node = enrolled.node.parse()?; - - sender - .send(NodeMessage { - sequence: 2, - message_id: "node-message-2".to_owned(), - idempotency_key: "node-message-2".to_owned(), - body: Some(node_message::Body::Hello(Hello { - node: enrolled.node, - credential: enrolled.credential, - capabilities: Some(Capabilities { - architecture: "arm64".to_owned(), - operating_system: "linux".to_owned(), - container_runtime: "docker".to_owned(), - container_runtime_version: "28.6.1".to_owned(), - cores: 2, - memory_bytes: 8 * 1024 * 1024 * 1024, - labels: vec!["self-hosted".to_owned(), "linux".to_owned()], - }), - capacity: Some(Capacity { - build_volume_free_bytes: 60 * 1024 * 1024 * 1024, - layer_store_bytes: 12 * 1024 * 1024 * 1024, - cache_volume_present: true, - cache_volume_total_bytes: 100, - cache_volume_used_bytes: 40, - cache_volume_path: "/var/cache/syncode".to_owned(), - cached_images: Vec::new(), - cached_actions: Vec::new(), - }), - max_parallel: 1, - })), - }) - .await?; - - let welcome = next(&mut inbound).await?; - assert!(matches!( - welcome.body, - Some(control_message::Body::Welcome(_)) - )); - - let assignment = next(&mut inbound).await?; - let Some(control_message::Body::Assignment(assignment)) = assignment.body else { - panic!("expected the plan the push produced, got {assignment:?}"); - }; - // The node is handed a plan, not the workflow file the push carried. - let plan: VersionedPlan = serde_json::from_slice(&assignment.plan)?; - assert_eq!(plan.schema(), syncode_workflow::PlanSchemaVersion::CURRENT); - - // A plan says nothing about what it is being built from, so the assignment - // states it: without this a job cannot check anything out. - let origin = assignment.origin.ok_or("the assignment stated no origin")?; - assert_eq!(origin.number, 1); - assert_eq!(origin.repository, "syncode/fixture"); - assert_eq!(origin.commit, COMMIT); - assert_eq!(origin.reference, "refs/heads/main"); - assert_eq!(origin.event, "push"); - assert_eq!( - assignment.secrets, - vec![syncode_control_node::REPOSITORY_TOKEN_SECRET] - ); - assert!(!assignment.secret_capability.is_empty()); - harness - .runs - .authorize_secret( - assignment.run.parse()?, - assignment.job.parse()?, - node, - Fence::from(assignment.fence), - syncode_control_node::REPOSITORY_TOKEN_SECRET, - ) - .await?; - - Ok(()) -} - -#[tokio::test] -async fn an_annotated_tag_push_uses_the_target_commit_and_tag_reference() -> TestResult { - let harness = start().await?; - let body = tag_body(); - - assert_eq!( - deliver(&harness, "repository.ref.updated", &body, true).await?, - 202 - ); - let assignment = harness - .runs - .take_next(NodeId::fresh()) - .await? - .ok_or("the tag push produced no run")?; - - assert_eq!(assignment.origin().commit(), COMMIT); - assert_eq!(assignment.origin().reference(), "refs/tags/v0.5.0"); - assert_eq!(assignment.origin().event(), "push"); - Ok(()) -} - -#[tokio::test] -async fn a_delivery_nobody_signed_queues_nothing() -> TestResult { - let harness = start().await?; - - let body = push_body(); - assert_eq!( - deliver(&harness, "repository.ref.updated", &body, false).await?, - 401 - ); - assert!( - harness - .runs - .take_next(syncode_control_runs::NodeId::fresh()) - .await? - .is_none() - ); - - Ok(()) -} - -#[tokio::test] -async fn a_retried_delivery_does_not_duplicate_its_run() -> TestResult { - let harness = start().await?; - let body = push_body(); - assert_eq!( - deliver(&harness, "repository.ref.updated", &body, true).await?, - 202 - ); - assert_eq!( - deliver(&harness, "repository.ref.updated", &body, true).await?, - 202 - ); - - let first = harness - .runs - .take_next(NodeId::fresh()) - .await? - .ok_or("the delivery produced no run")?; - assert!(harness.runs.take_next(NodeId::fresh()).await?.is_none()); - assert_eq!(first.origin().event(), "push"); - Ok(()) -} - -#[tokio::test] -async fn an_event_the_workflow_does_not_declare_is_accepted_and_ignored() -> TestResult { - let harness = start().await?; - - let body = push_body(); - assert_eq!(deliver(&harness, "issues", &body, true).await?, 202); - assert!( - harness - .runs - .take_next(syncode_control_runs::NodeId::fresh()) - .await? - .is_none() - ); - - Ok(()) -} - -#[tokio::test] -async fn a_legacy_forge_push_is_accepted_without_a_duplicate_run() -> TestResult { - let harness = start().await?; - let body = legacy_push_body(); - assert_eq!(deliver(&harness, "push", &body, true).await?, 202); - assert!(harness.runs.take_next(NodeId::fresh()).await?.is_none()); - Ok(()) -} - -fn pull_request_body(action: &str) -> String { - format!( - r#"{{"action":"{action}","number":7,"repository":{{"full_name":"syncode/demo"}},"pull_request":{{"head":{{"sha":"{COMMIT}"}},"base":{{"ref":"main"}}}}}}"# - ) -} - -#[tokio::test] -async fn a_pull_request_becomes_a_run_using_the_files_it_touches() -> TestResult { - let harness = start().await?; - - let body = pull_request_body("opened"); - assert_eq!(deliver(&harness, "pull_request", &body, true).await?, 202); - - // The workflow filters on `paths: src/**`. The webhook body carries no file - // list, so unless the control plane asks the forge for one, this run does - // not exist and nothing says why. - let assignment = harness - .runs - .take_next(syncode_control_runs::NodeId::fresh()) - .await? - .ok_or("the pull request produced no run")?; - let plan: VersionedPlan = serde_json::from_slice(assignment.plan())?; - assert_eq!(plan.schema(), syncode_workflow::PlanSchemaVersion::CURRENT); - - // A pull request head sits on no branch this control plane knows, but the - // forge publishes it under the pull request, which is what a checkout can - // fetch. - assert_eq!(assignment.origin().reference(), "refs/pull/7/head"); - assert_eq!(assignment.origin().event(), "pull_request"); - Ok(()) -} - -#[tokio::test] -async fn a_repository_plane_pull_request_reads_content_over_grpc() -> TestResult { - let harness = start().await?; - - let body = pull_request_body("opened"); - assert_eq!(deliver_native_pull_request(&harness, &body).await?, 202); - - let assignment = harness - .runs - .take_next(syncode_control_runs::NodeId::fresh()) - .await? - .ok_or("the native pull request produced no run")?; - assert_eq!(assignment.origin().repository(), "syncode/demo"); - assert_eq!(assignment.origin().reference(), "refs/pull/7/head"); - Ok(()) -} - -#[tokio::test] -async fn closing_a_pull_request_starts_nothing() -> TestResult { - let harness = start().await?; - - let body = pull_request_body("closed"); - assert_eq!(deliver(&harness, "pull_request", &body, true).await?, 202); - assert!( - harness - .runs - .take_next(syncode_control_runs::NodeId::fresh()) - .await? - .is_none() - ); - Ok(()) -} - -#[tokio::test] -async fn manual_and_scheduled_deliveries_become_runs() -> TestResult { - for event in ["workflow_dispatch", "schedule"] { - let harness = start().await?; - let body = format!( - r#"{{"ref":"refs/heads/main","after":"{COMMIT}","workflow":".gitea/workflows/ci.yml","repository":{{"full_name":"syncode/demo"}}}}"# - ); - assert_eq!(deliver(&harness, event, &body, true).await?, 202); - let assignment = harness - .runs - .take_next(syncode_control_runs::NodeId::fresh()) - .await? - .ok_or("event produced no run")?; - assert_eq!(assignment.origin().event(), event); - assert_eq!(assignment.origin().reference(), "refs/heads/main"); - assert!( - harness - .runs - .take_next(syncode_control_runs::NodeId::fresh()) - .await? - .is_none(), - "a requested event started a workflow other than the selected file" - ); - } - Ok(()) -} - -#[tokio::test] -async fn a_manual_delivery_can_select_a_tag() -> TestResult { - let harness = start().await?; - let body = format!( - r#"{{"ref":"refs/tags/v0.4.0","after":"{COMMIT}","workflow":".gitea/workflows/ci.yml","repository":{{"full_name":"syncode/demo"}}}}"# - ); - - assert_eq!( - deliver(&harness, "workflow_dispatch", &body, true).await?, - 202 - ); - let assignment = harness - .runs - .take_next(NodeId::fresh()) - .await? - .ok_or("manual tag event produced no run")?; - assert_eq!(assignment.origin().reference(), "refs/tags/v0.4.0"); - Ok(()) -} - -#[tokio::test] -async fn shadow_mode_persists_the_run_but_never_assigns_it() -> TestResult { - let harness = start_mode(true).await?; - let body = push_body(); - assert_eq!( - deliver(&harness, "repository.ref.updated", &body, true).await?, - 202 - ); - let (_sender, mut inbound) = open_node(&harness).await?; - - assert!( - tokio::time::timeout(Duration::from_millis(100), inbound.next()) - .await - .is_err(), - "shadow mode must not assign the run" - ); - assert!( - harness.runs.take_next(NodeId::fresh()).await?.is_some(), - "the shadow run must still exist in the aggregate" - ); - Ok(()) -} +#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)] + +#[path = "support/actions.rs"] +mod actions; +#[path = "support/repository.rs"] +mod repository; + +use std::error::Error; +use std::sync::Arc; +use std::time::Duration; + +use base64::Engine; +use base64::engine::general_purpose::STANDARD; +use hmac::{Hmac, KeyInit, Mac}; +use sha2::Sha256; +use syncode_control::repository::RepositoryContents; +use syncode_control::repository_grpc::NativeRepositoryContents; +use syncode_control::repository_sources::RepositorySources; +use syncode_control::webhook::{Intake, router}; +use syncode_control_node::identity_wire::identity_server::{Identity, IdentityServer}; +use syncode_control_node::identity_wire::{ + CheckCapabilityRequest, CheckCapabilityResponse, GetRepositoryCoordinatesRequest, + GetRepositoryCoordinatesResponse, IssueWorkflowRepositoryTokenRequest, + IssueWorkflowRepositoryTokenResponse, ResolveRepositoryRequest, ResolveRepositoryResponse, + ValidateSessionRequest, ValidateSessionResponse, +}; +use syncode_control_node::wire::{ + Capabilities, Capacity, ControlMessage, Enrol, Hello, NodeMessage, control_message, + node_message, +}; +use syncode_control_node::{ + ArtifactTokenAuthority, CapabilityAuthority, GeneratedServer, NodeSessionClient, + NodeSessionServer, RepositoryCoordinates, +}; +use syncode_control_nodes::{Ephemeral, Nodes, Scope}; +use syncode_control_runs::{Fence, Forgotten, NodeId, Runs}; +use syncode_workflow::VersionedPlan; +use syncode_workflow_github_actions::expression::ExpressionProgram; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpListener; +use tokio::sync::mpsc; +use tokio_stream::StreamExt; +use tokio_stream::wrappers::{ReceiverStream, TcpListenerStream}; +use tonic::transport::Server; +use tonic::{Request, Response, Status, Streaming}; +use url::Url; + +use actions::FIXTURE_ACTIONS; + +type TestResult = Result>; + +struct ProjectionIdentity; + +#[tonic::async_trait] +impl Identity for ProjectionIdentity { + async fn issue_workflow_repository_token( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("issue_workflow_repository_token")) + } + + async fn validate_session( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("validate_session")) + } + + async fn check_capability( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("check_capability")) + } + + async fn resolve_repository( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("resolve_repository")) + } + + async fn get_repository_coordinates( + &self, + _request: Request, + ) -> Result, Status> { + Ok(Response::new(GetRepositoryCoordinatesResponse { + owner: "syncode".to_owned(), + name: "fixture".to_owned(), + })) + } +} + +const SECRET: &str = "the secret the hook was configured with"; +const COMMIT: &str = "9f2c1e4a7b3d5f6081a2c3d4e5f60718293a4b5c"; + +const WORKFLOW: &str = r#" +name: CI +on: + push: + branches: [main] + tags: ["v*"] + pull_request: + branches: [main] + paths: + - "src/**" + workflow_dispatch: + schedule: + - cron: "0 6 * * *" +jobs: + build: + runs-on: [self-hosted, linux] + steps: + - run: echo "the run came from a push" +"#; + +const OTHER_MANUAL_WORKFLOW: &str = r#" +name: Other manual workflow +on: + push: + workflow_dispatch: +jobs: + other: + runs-on: [self-hosted, linux] + steps: + - run: echo "the other workflow must not run" +"#; + +/// A forge that serves one workflow directory and has nothing in the other. +async fn forge() -> TestResult { + let listener = TcpListener::bind("127.0.0.1:0").await?; + let base = Url::parse(&format!("http://{}/", listener.local_addr()?))?; + let listing = format!( + r#"[{{"path":".gitea/workflows/ci.yml","type":"file","content":"{}"}},{{"path":".gitea/workflows/other.yml","type":"file","content":"{}"}}]"#, + STANDARD.encode(WORKFLOW), + STANDARD.encode(OTHER_MANUAL_WORKFLOW) + ); + + tokio::spawn(async move { + // Answers by what was asked for rather than in a fixed order: a push + // never asks for a file list, a pull request does. + let files = r#"[{"filename":"src/main.rs"}]"#.to_owned(); + loop { + let Ok((mut stream, _)) = listener.accept().await else { + return; + }; + let mut buffer = vec![0_u8; 4096]; + let read = stream.read(&mut buffer).await.unwrap_or(0); + let request = String::from_utf8_lossy(&buffer[..read]).to_string(); + let asked = request.lines().next().unwrap_or_default().to_owned(); + + let (status, body) = if asked.contains("/pulls/") { + (200, files.clone()) + } else if asked.contains(".gitea%2Fworkflows") || asked.contains(".gitea/workflows") { + (200, listing.clone()) + } else { + (404, String::from("{}")) + }; + + let response = format!( + "HTTP/1.1 {status} X\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}", + body.len() + ); + let _ = stream.write_all(response.as_bytes()).await; + let _ = stream.shutdown().await; + } + }); + + Ok(base) +} + +struct Harness { + runs: Runs, + nodes: Nodes, + session: String, + events: String, +} + +/// Both ends of the control plane over one set of registries, exactly as the +/// binary wires them. +async fn start() -> TestResult { + start_mode(false).await +} + +async fn start_mode(shadow: bool) -> TestResult { + let runs = Runs::restored(Forgotten::default()).await?; + let nodes = Nodes::restored(Ephemeral::default()).await?; + let repositories = repository_coordinates().await?; + + let sessions = TcpListener::bind("127.0.0.1:0").await?; + let session = format!("http://{}", sessions.local_addr()?); + let served = (runs.clone(), nodes.clone()); + tokio::spawn(async move { + let session = if shadow { + NodeSessionServer::shadow( + served.0, + served.1, + capability_authority(), + artifact_authority(), + "http://127.0.0.1:1/".parse().expect("artifact URL"), + repositories, + ) + } else { + NodeSessionServer::new( + served.0, + served.1, + capability_authority(), + artifact_authority(), + "http://127.0.0.1:1/".parse().expect("artifact URL"), + repositories, + ) + }; + let _ = Server::builder() + .add_service(GeneratedServer::new(session)) + .serve_with_incoming(TcpListenerStream::new(sessions)) + .await; + }); + + let repository_listener = TcpListener::bind("127.0.0.1:0").await?; + let repository_endpoint = format!("http://{}", repository_listener.local_addr()?); + tokio::spawn(async move { + let _ = Server::builder() + .add_service(repository::service()) + .serve_with_incoming(TcpListenerStream::new(repository_listener)) + .await; + }); + let native = NativeRepositoryContents::connect(repository_endpoint).await?; + + let deliveries = TcpListener::bind("127.0.0.1:0").await?; + let events = format!("http://{}", deliveries.local_addr()?); + let intake = Arc::new(Intake::new( + RepositorySources::new( + native, + RepositoryContents::new(forge().await?, "a repository token".to_owned()), + ), + runs.clone(), + FIXTURE_ACTIONS, + SECRET.to_owned(), + )); + tokio::spawn(async move { + let _ = axum::serve(deliveries, router(intake)).await; + }); + + Ok(Harness { + runs, + nodes, + session, + events, + }) +} + +fn capability_authority() -> CapabilityAuthority { + CapabilityAuthority::new("test-capability-key").expect("capability key") +} + +fn artifact_authority() -> ArtifactTokenAuthority { + ArtifactTokenAuthority::new("test-artifact-key").expect("artifact key") +} + +async fn repository_coordinates() -> TestResult { + let identity_listener = TcpListener::bind("127.0.0.1:0").await?; + let identity_endpoint = format!("http://{}", identity_listener.local_addr()?); + tokio::spawn(async move { + let _ = Server::builder() + .add_service(IdentityServer::new(ProjectionIdentity)) + .serve_with_incoming(TcpListenerStream::new(identity_listener)) + .await; + }); + Ok(RepositoryCoordinates::new( + identity_endpoint, + String::new(), + )?) +} + +async fn open_node( + harness: &Harness, +) -> TestResult<(mpsc::Sender, Streaming)> { + let (sender, receiver) = mpsc::channel(8); + let mut client = NodeSessionClient::connect(harness.session.clone()).await?; + let mut inbound = client + .open(ReceiverStream::new(receiver)) + .await? + .into_inner(); + let token = harness.nodes.issue_token(Scope::Instance).await?; + sender + .send(NodeMessage { + sequence: 1, + message_id: "node-message-1".to_owned(), + idempotency_key: "node-message-1".to_owned(), + body: Some(node_message::Body::Enrol(Enrol { + token: token.secret().expose().to_owned(), + })), + }) + .await?; + let message = next(&mut inbound).await?; + let Some(control_message::Body::Enrolled(enrolled)) = message.body else { + return Err("expected an enrolled node".into()); + }; + sender + .send(NodeMessage { + sequence: 2, + message_id: "node-message-2".to_owned(), + idempotency_key: "node-message-2".to_owned(), + body: Some(node_message::Body::Hello(Hello { + node: enrolled.node, + credential: enrolled.credential, + capabilities: Some(Capabilities { + architecture: "arm64".to_owned(), + operating_system: "linux".to_owned(), + container_runtime: "docker".to_owned(), + container_runtime_version: "28.6.1".to_owned(), + cores: 2, + memory_bytes: 8 * 1024 * 1024 * 1024, + labels: vec!["self-hosted".to_owned(), "linux".to_owned()], + }), + capacity: Some(Capacity { + build_volume_free_bytes: 60 * 1024 * 1024 * 1024, + layer_store_bytes: 12 * 1024 * 1024 * 1024, + cache_volume_present: true, + cache_volume_total_bytes: 100, + cache_volume_used_bytes: 40, + cache_volume_path: "/var/cache/syncode".to_owned(), + cached_images: Vec::new(), + cached_actions: Vec::new(), + }), + max_parallel: 1, + })), + }) + .await?; + let welcome = next(&mut inbound).await?; + if !matches!(welcome.body, Some(control_message::Body::Welcome(_))) { + return Err("expected a welcome".into()); + } + Ok((sender, inbound)) +} + +fn push_body() -> String { + format!( + r#"{{"message_id":"018f47e2-b2c4-7f19-8a6d-13ef76c89211","protocol_version":"1.0","schema_version":1,"source_node_id":"018f47e2-b2c4-7f19-8a6d-13ef76c89212","repository_id":"{}","message_type":"repository.ref.updated","sequence":2,"term":1,"payload":{{"request":{{"principal_id":"018f47e2-b2c4-7f19-8a6d-13ef76c89213"}},"changes":[{{"name_hex":"726566732f68656164732f6d61696e","old":{{"kind":"object","object_id":"1111111111111111111111111111111111111111"}},"new":{{"kind":"object","object_id":"{COMMIT}"}}}}]}}}}"#, + repository::REPOSITORY + ) +} + +fn tag_body() -> String { + format!( + r#"{{"message_id":"018f47e2-b2c4-7f19-8a6d-13ef76c89211","protocol_version":"1.0","schema_version":1,"source_node_id":"018f47e2-b2c4-7f19-8a6d-13ef76c89212","repository_id":"{}","message_type":"repository.ref.updated","sequence":2,"term":1,"payload":{{"request":{{"principal_id":"018f47e2-b2c4-7f19-8a6d-13ef76c89213"}},"changes":[{{"name_hex":"726566732f746167732f76302e352e30","old":null,"new":{{"kind":"object","object_id":"{}"}}}}]}}}}"#, + repository::REPOSITORY, + repository::TAG_OBJECT + ) +} + +fn legacy_push_body() -> String { + format!( + r#"{{"ref":"refs/heads/main","after":"{COMMIT}","repository":{{"full_name":"syncode/demo"}},"commits":[{{"modified":["src/main.rs"]}}]}}"# + ) +} + +fn signature(body: &str) -> String { + let mut mac = Hmac::::new_from_slice(SECRET.as_bytes()).expect("key"); + mac.update(body.as_bytes()); + mac.finalize() + .into_bytes() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +async fn deliver(harness: &Harness, event: &str, body: &str, signed: bool) -> TestResult { + let signature = if signed { + signature(body) + } else { + signature("something else entirely") + }; + let response = reqwest::Client::new() + .post(format!("{}/events", harness.events)) + .header("x-syncode-event", event) + .header("x-syncode-signature", signature) + .header("x-syncode-delivery", format!("{event}-delivery")) + .header( + "x-syncode-workflow", + if event == "repository.ref.updated" { + ".gitea/workflows/ci.yml" + } else { + "" + }, + ) + .body(body.to_owned()) + .send() + .await?; + Ok(response.status().as_u16()) +} + +async fn deliver_native_pull_request(harness: &Harness, body: &str) -> TestResult { + let response = reqwest::Client::new() + .post(format!("{}/events", harness.events)) + .header("x-syncode-event", "pull_request") + .header("x-syncode-signature", signature(body)) + .header("x-syncode-delivery", "native-pull-request-delivery") + .header("x-syncode-repository-id", repository::REPOSITORY) + .header( + "x-syncode-before", + "1111111111111111111111111111111111111111", + ) + .header("x-syncode-after", COMMIT) + .body(body.to_owned()) + .send() + .await?; + Ok(response.status().as_u16()) +} + +async fn next(stream: &mut Streaming) -> TestResult { + Ok(stream + .next() + .await + .ok_or("the control plane closed the stream")??) +} + +#[tokio::test] +async fn a_push_becomes_a_plan_the_node_is_handed() -> TestResult { + let harness = start().await?; + + let body = push_body(); + assert_eq!( + deliver(&harness, "repository.ref.updated", &body, true).await?, + 202 + ); + + let (sender, receiver) = mpsc::channel(8); + let mut client = NodeSessionClient::connect(harness.session.clone()).await?; + let mut inbound = client + .open(ReceiverStream::new(receiver)) + .await? + .into_inner(); + + let token = harness.nodes.issue_token(Scope::Instance).await?; + sender + .send(NodeMessage { + sequence: 1, + message_id: "node-message-1".to_owned(), + idempotency_key: "node-message-1".to_owned(), + body: Some(node_message::Body::Enrol(Enrol { + token: token.secret().expose().to_owned(), + })), + }) + .await?; + let message = next(&mut inbound).await?; + let Some(control_message::Body::Enrolled(enrolled)) = message.body else { + panic!("expected an identity, got {:?}", message.body); + }; + let node = enrolled.node.parse()?; + + sender + .send(NodeMessage { + sequence: 2, + message_id: "node-message-2".to_owned(), + idempotency_key: "node-message-2".to_owned(), + body: Some(node_message::Body::Hello(Hello { + node: enrolled.node, + credential: enrolled.credential, + capabilities: Some(Capabilities { + architecture: "arm64".to_owned(), + operating_system: "linux".to_owned(), + container_runtime: "docker".to_owned(), + container_runtime_version: "28.6.1".to_owned(), + cores: 2, + memory_bytes: 8 * 1024 * 1024 * 1024, + labels: vec!["self-hosted".to_owned(), "linux".to_owned()], + }), + capacity: Some(Capacity { + build_volume_free_bytes: 60 * 1024 * 1024 * 1024, + layer_store_bytes: 12 * 1024 * 1024 * 1024, + cache_volume_present: true, + cache_volume_total_bytes: 100, + cache_volume_used_bytes: 40, + cache_volume_path: "/var/cache/syncode".to_owned(), + cached_images: Vec::new(), + cached_actions: Vec::new(), + }), + max_parallel: 1, + })), + }) + .await?; + + let welcome = next(&mut inbound).await?; + assert!(matches!( + welcome.body, + Some(control_message::Body::Welcome(_)) + )); + + let assignment = next(&mut inbound).await?; + let Some(control_message::Body::Assignment(assignment)) = assignment.body else { + panic!("expected the plan the push produced, got {assignment:?}"); + }; + // The node is handed a plan, not the workflow file the push carried. + let plan: VersionedPlan = serde_json::from_slice(&assignment.plan)?; + assert_eq!(plan.schema(), syncode_workflow::PlanSchemaVersion::CURRENT); + + // A plan says nothing about what it is being built from, so the assignment + // states it: without this a job cannot check anything out. + let origin = assignment.origin.ok_or("the assignment stated no origin")?; + assert_eq!(origin.number, 1); + assert_eq!(origin.repository, "syncode/fixture"); + assert_eq!(origin.commit, COMMIT); + assert_eq!(origin.reference, "refs/heads/main"); + assert_eq!(origin.event, "push"); + assert_eq!( + assignment.secrets, + vec![syncode_control_node::REPOSITORY_TOKEN_SECRET] + ); + assert!(!assignment.secret_capability.is_empty()); + harness + .runs + .authorize_secret( + assignment.run.parse()?, + assignment.job.parse()?, + node, + Fence::from(assignment.fence), + syncode_control_node::REPOSITORY_TOKEN_SECRET, + ) + .await?; + + Ok(()) +} + +#[tokio::test] +async fn an_annotated_tag_push_uses_the_target_commit_and_tag_reference() -> TestResult { + let harness = start().await?; + let body = tag_body(); + + assert_eq!( + deliver(&harness, "repository.ref.updated", &body, true).await?, + 202 + ); + let assignment = harness + .runs + .take_next(NodeId::fresh()) + .await? + .ok_or("the tag push produced no run")?; + + assert_eq!(assignment.origin().commit(), COMMIT); + assert_eq!(assignment.origin().reference(), "refs/tags/v0.5.0"); + assert_eq!(assignment.origin().event(), "push"); + Ok(()) +} + +#[tokio::test] +async fn a_delivery_nobody_signed_queues_nothing() -> TestResult { + let harness = start().await?; + + let body = push_body(); + assert_eq!( + deliver(&harness, "repository.ref.updated", &body, false).await?, + 401 + ); + assert!( + harness + .runs + .take_next(syncode_control_runs::NodeId::fresh()) + .await? + .is_none() + ); + + Ok(()) +} + +#[tokio::test] +async fn a_retried_delivery_does_not_duplicate_its_run() -> TestResult { + let harness = start().await?; + let body = push_body(); + assert_eq!( + deliver(&harness, "repository.ref.updated", &body, true).await?, + 202 + ); + assert_eq!( + deliver(&harness, "repository.ref.updated", &body, true).await?, + 202 + ); + + let first = harness + .runs + .take_next(NodeId::fresh()) + .await? + .ok_or("the delivery produced no run")?; + assert!(harness.runs.take_next(NodeId::fresh()).await?.is_none()); + assert_eq!(first.origin().event(), "push"); + Ok(()) +} + +#[tokio::test] +async fn an_event_the_workflow_does_not_declare_is_accepted_and_ignored() -> TestResult { + let harness = start().await?; + + let body = push_body(); + assert_eq!(deliver(&harness, "issues", &body, true).await?, 202); + assert!( + harness + .runs + .take_next(syncode_control_runs::NodeId::fresh()) + .await? + .is_none() + ); + + Ok(()) +} + +#[tokio::test] +async fn a_legacy_forge_push_is_accepted_without_a_duplicate_run() -> TestResult { + let harness = start().await?; + let body = legacy_push_body(); + assert_eq!(deliver(&harness, "push", &body, true).await?, 202); + assert!(harness.runs.take_next(NodeId::fresh()).await?.is_none()); + Ok(()) +} + +fn pull_request_body(action: &str) -> String { + format!( + r#"{{"action":"{action}","number":7,"repository":{{"full_name":"syncode/demo"}},"pull_request":{{"head":{{"sha":"{COMMIT}"}},"base":{{"ref":"main"}}}}}}"# + ) +} + +#[tokio::test] +async fn a_pull_request_becomes_a_run_using_the_files_it_touches() -> TestResult { + let harness = start().await?; + + let body = pull_request_body("opened"); + assert_eq!(deliver(&harness, "pull_request", &body, true).await?, 202); + + // The workflow filters on `paths: src/**`. The webhook body carries no file + // list, so unless the control plane asks the forge for one, this run does + // not exist and nothing says why. + let assignment = harness + .runs + .take_next(syncode_control_runs::NodeId::fresh()) + .await? + .ok_or("the pull request produced no run")?; + let plan: VersionedPlan = serde_json::from_slice(assignment.plan())?; + assert_eq!(plan.schema(), syncode_workflow::PlanSchemaVersion::CURRENT); + + // A pull request head sits on no branch this control plane knows, but the + // forge publishes it under the pull request, which is what a checkout can + // fetch. + assert_eq!(assignment.origin().reference(), "refs/pull/7/head"); + assert_eq!(assignment.origin().event(), "pull_request"); + Ok(()) +} + +#[tokio::test] +async fn a_repository_plane_pull_request_reads_content_over_grpc() -> TestResult { + let harness = start().await?; + + let body = pull_request_body("opened"); + assert_eq!(deliver_native_pull_request(&harness, &body).await?, 202); + + let assignment = harness + .runs + .take_next(syncode_control_runs::NodeId::fresh()) + .await? + .ok_or("the native pull request produced no run")?; + assert_eq!(assignment.origin().repository(), "syncode/demo"); + assert_eq!(assignment.origin().reference(), "refs/pull/7/head"); + Ok(()) +} + +#[tokio::test] +async fn closing_a_pull_request_starts_nothing() -> TestResult { + let harness = start().await?; + + let body = pull_request_body("closed"); + assert_eq!(deliver(&harness, "pull_request", &body, true).await?, 202); + assert!( + harness + .runs + .take_next(syncode_control_runs::NodeId::fresh()) + .await? + .is_none() + ); + Ok(()) +} + +#[tokio::test] +async fn manual_and_scheduled_deliveries_become_runs() -> TestResult { + for event in ["workflow_dispatch", "schedule"] { + let harness = start().await?; + let body = format!( + r#"{{"ref":"refs/heads/main","after":"{COMMIT}","workflow":".gitea/workflows/ci.yml","repository":{{"full_name":"syncode/demo"}}}}"# + ); + assert_eq!(deliver(&harness, event, &body, true).await?, 202); + let assignment = harness + .runs + .take_next(syncode_control_runs::NodeId::fresh()) + .await? + .ok_or("event produced no run")?; + assert_eq!(assignment.origin().event(), event); + assert_eq!(assignment.origin().reference(), "refs/heads/main"); + assert!( + harness + .runs + .take_next(syncode_control_runs::NodeId::fresh()) + .await? + .is_none(), + "a requested event started a workflow other than the selected file" + ); + } + Ok(()) +} + +#[tokio::test] +async fn a_manual_delivery_can_select_a_tag() -> TestResult { + let harness = start().await?; + let body = format!( + r#"{{"ref":"refs/tags/v0.4.0","after":"{COMMIT}","workflow":".gitea/workflows/ci.yml","repository":{{"full_name":"syncode/demo"}}}}"# + ); + + assert_eq!( + deliver(&harness, "workflow_dispatch", &body, true).await?, + 202 + ); + let assignment = harness + .runs + .take_next(NodeId::fresh()) + .await? + .ok_or("manual tag event produced no run")?; + assert_eq!(assignment.origin().reference(), "refs/tags/v0.4.0"); + Ok(()) +} + +#[tokio::test] +async fn shadow_mode_persists_the_run_but_never_assigns_it() -> TestResult { + let harness = start_mode(true).await?; + let body = push_body(); + assert_eq!( + deliver(&harness, "repository.ref.updated", &body, true).await?, + 202 + ); + let (_sender, mut inbound) = open_node(&harness).await?; + + assert!( + tokio::time::timeout(Duration::from_millis(100), inbound.next()) + .await + .is_err(), + "shadow mode must not assign the run" + ); + assert!( + harness.runs.take_next(NodeId::fresh()).await?.is_some(), + "the shadow run must still exist in the aggregate" + ); + Ok(()) +} diff --git a/crates/control-node/Cargo.toml b/crates/control-node/Cargo.toml --- a/crates/control-node/Cargo.toml +++ b/crates/control-node/Cargo.toml @@ -1,35 +1,34 @@ -[package] -name = "syncode-control-node" -description = "The gRPC stream a node opens to the control plane" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -repository.workspace = true -publish = false - -[dependencies] -base64 = "0.22.1" -hmac = "0.12.1" -prost = "0.14.4" -reqwest = { version = "0.13.4", default-features = false, features = ["json", "rustls"] } -serde = { version = "1.0.229", features = ["derive"] } -serde_json = "1.0.149" -sha2 = "0.10.9" -syncode-control-nodes = { path = "../control-nodes" } -syncode-control-runs = { path = "../control-runs" } -thiserror = "2.0.19" -tokio = { version = "1.49.0", features = ["sync"] } -tokio-stream = "0.1.18" -tonic = "0.14.6" -tonic-prost = "0.14.6" -url = "2.5.8" -uuid = { version = "1.24.0", features = ["v4"] } - -[build-dependencies] -prost-build = "0.14.4" -protoc-bin-vendored = "3.2.0" -tonic-prost-build = "0.14.6" - -[lints] -workspace = true +[package] +name = "syncode-control-node" +description = "The gRPC stream a node opens to the control plane" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +publish = false + +[dependencies] +base64 = "0.22.1" +hmac = "0.12.1" +prost = "0.14.4" +serde = { version = "1.0.229", features = ["derive"] } +serde_json = "1.0.149" +sha2 = "0.10.9" +syncode-control-nodes = { path = "../control-nodes" } +syncode-control-runs = { path = "../control-runs" } +thiserror = "2.0.19" +tokio = { version = "1.49.0", features = ["sync"] } +tokio-stream = "0.1.18" +tonic = "0.14.6" +tonic-prost = "0.14.6" +url = "2.5.8" +uuid = { version = "1.24.0", features = ["v4"] } + +[build-dependencies] +prost-build = "0.14.4" +protoc-bin-vendored = "3.2.0" +tonic-prost-build = "0.14.6" + +[lints] +workspace = true diff --git a/crates/control-node/src/error.rs b/crates/control-node/src/error.rs --- a/crates/control-node/src/error.rs +++ b/crates/control-node/src/error.rs @@ -1,110 +1,106 @@ -use prost::DecodeError; -use syncode_control_nodes::NodesError; -use syncode_control_runs::{IdentityError, RunsError}; -use thiserror::Error; - -use crate::ArtifactTokenError; -use crate::ProjectionRequestError; -use tonic::Status; - -use crate::CapabilityError; -use crate::declaration::DeclarationError; - -#[derive(Debug, Error)] -pub enum SessionError { - #[error("node message has no message_id or idempotency_key")] - Unidentified, - - #[error("node message_id {0:?} was reused for different content")] - MessageIdConflict(String), - - #[error("node idempotency_key {0:?} was reused for different content")] - IdempotencyConflict(String), - - #[error("node declared invalid parallelism {0}")] - InvalidParallelism(u32), - - #[error("node {0} already has an active session")] - ConcurrentSession(syncode_control_runs::NodeId), - - #[error("node message sequence {actual} arrived while {expected} was required")] - OutOfSequence { expected: u64, actual: u64 }, - #[error(transparent)] - Identity(#[from] IdentityError), - - #[error(transparent)] - Service(#[from] RunsError), - - #[error(transparent)] - Nodes(#[from] NodesError), - - #[error(transparent)] - Declaration(#[from] DeclarationError), - - #[error(transparent)] - Capability(#[from] CapabilityError), - - #[error(transparent)] - ArtifactToken(#[from] ArtifactTokenError), - - #[error(transparent)] - Projection(#[from] ProjectionRequestError), - - #[error("run {0} has no projection before dispatch")] - MissingProjection(syncode_control_runs::RunId), - - #[error("a node must say what it can do and how much room it has")] - Undeclared, - - #[error("the node has not said hello on this stream")] - NoHello, - - #[error("node message carried no body")] - EmptyMessage, - - #[error("progress reported an unknown conclusion {0}")] - UnknownConclusion(i32), - - #[error("the node stopped reading its stream")] - Unreadable, - - #[error(transparent)] - Transport(#[from] Status), - - #[error("a stored control message cannot be decoded: {0}")] - Outbox(DecodeError), - - #[error("durable control message {0:?} disappeared from the outbox")] - MissingOutbox(String), -} - -impl From for Status { - fn from(error: SessionError) -> Self { - match error { - SessionError::Transport(status) => status, - SessionError::Unidentified - | SessionError::MessageIdConflict(_) - | SessionError::IdempotencyConflict(_) - | SessionError::Identity(_) - | SessionError::EmptyMessage - | SessionError::InvalidParallelism(_) - | SessionError::OutOfSequence { .. } - | SessionError::UnknownConclusion(_) => Self::invalid_argument(error.to_string()), - SessionError::NoHello | SessionError::ConcurrentSession(_) => { - Self::failed_precondition(error.to_string()) - } - SessionError::Unreadable => Self::unavailable(error.to_string()), - SessionError::Undeclared | SessionError::Declaration(_) => { - Self::invalid_argument(error.to_string()) - } - SessionError::Capability(_) - | SessionError::ArtifactToken(_) - | SessionError::Projection(_) - | SessionError::MissingProjection(_) => Self::internal(error.to_string()), - SessionError::Nodes(_) => Self::permission_denied(error.to_string()), - SessionError::Service(_) | SessionError::Outbox(_) | SessionError::MissingOutbox(_) => { - Self::internal(error.to_string()) - } - } - } -} +use prost::DecodeError; +use syncode_control_nodes::NodesError; +use syncode_control_runs::{IdentityError, RunsError}; +use thiserror::Error; + +use crate::ArtifactTokenError; +use crate::RepositoryCoordinateRequestError; +use tonic::Status; + +use crate::CapabilityError; +use crate::declaration::DeclarationError; + +#[derive(Debug, Error)] +pub enum SessionError { + #[error("node message has no message_id or idempotency_key")] + Unidentified, + + #[error("node message_id {0:?} was reused for different content")] + MessageIdConflict(String), + + #[error("node idempotency_key {0:?} was reused for different content")] + IdempotencyConflict(String), + + #[error("node declared invalid parallelism {0}")] + InvalidParallelism(u32), + + #[error("node {0} already has an active session")] + ConcurrentSession(syncode_control_runs::NodeId), + + #[error("node message sequence {actual} arrived while {expected} was required")] + OutOfSequence { expected: u64, actual: u64 }, + #[error(transparent)] + Identity(#[from] IdentityError), + + #[error(transparent)] + Service(#[from] RunsError), + + #[error(transparent)] + Nodes(#[from] NodesError), + + #[error(transparent)] + Declaration(#[from] DeclarationError), + + #[error(transparent)] + Capability(#[from] CapabilityError), + + #[error(transparent)] + ArtifactToken(#[from] ArtifactTokenError), + + #[error(transparent)] + RepositoryCoordinates(#[from] RepositoryCoordinateRequestError), + + #[error("a node must say what it can do and how much room it has")] + Undeclared, + + #[error("the node has not said hello on this stream")] + NoHello, + + #[error("node message carried no body")] + EmptyMessage, + + #[error("progress reported an unknown conclusion {0}")] + UnknownConclusion(i32), + + #[error("the node stopped reading its stream")] + Unreadable, + + #[error(transparent)] + Transport(#[from] Status), + + #[error("a stored control message cannot be decoded: {0}")] + Outbox(DecodeError), + + #[error("durable control message {0:?} disappeared from the outbox")] + MissingOutbox(String), +} + +impl From for Status { + fn from(error: SessionError) -> Self { + match error { + SessionError::Transport(status) => status, + SessionError::Unidentified + | SessionError::MessageIdConflict(_) + | SessionError::IdempotencyConflict(_) + | SessionError::Identity(_) + | SessionError::EmptyMessage + | SessionError::InvalidParallelism(_) + | SessionError::OutOfSequence { .. } + | SessionError::UnknownConclusion(_) => Self::invalid_argument(error.to_string()), + SessionError::NoHello | SessionError::ConcurrentSession(_) => { + Self::failed_precondition(error.to_string()) + } + SessionError::Unreadable => Self::unavailable(error.to_string()), + SessionError::Undeclared | SessionError::Declaration(_) => { + Self::invalid_argument(error.to_string()) + } + SessionError::Capability(_) + | SessionError::ArtifactToken(_) + | SessionError::RepositoryCoordinates(_) => Self::internal(error.to_string()), + SessionError::Nodes(_) => Self::permission_denied(error.to_string()), + SessionError::Service(_) | SessionError::Outbox(_) | SessionError::MissingOutbox(_) => { + Self::internal(error.to_string()) + } + } + } +} diff --git a/crates/control-node/src/lib.rs b/crates/control-node/src/lib.rs --- a/crates/control-node/src/lib.rs +++ b/crates/control-node/src/lib.rs @@ -1,43 +1,45 @@ -mod artifact; -mod capability; -mod declaration; -mod error; -mod identity; -mod outbound; -mod projection; -mod reports; -mod session; - -pub mod wire { - //! Generated from `proto/node.proto`. - #![allow(clippy::doc_markdown, clippy::large_enum_variant, clippy::use_self)] - - include!(concat!(env!("OUT_DIR"), "/syncode.node.v1.rs")); -} - -pub mod identity_wire { - #![allow(clippy::doc_markdown, clippy::large_enum_variant, clippy::use_self)] - - include!(concat!(env!("OUT_DIR"), "/syncode.identity.v1.rs")); -} - -pub mod actions_wire { - #![allow(clippy::doc_markdown, clippy::large_enum_variant, clippy::use_self)] - - include!(concat!(env!("OUT_DIR"), "/syncode.control.v1.rs")); -} - -pub use actions_wire::actions_read_server::ActionsRead as ActionsReadService; -pub use actions_wire::actions_read_server::ActionsReadServer as GeneratedActionsReadServer; -pub use artifact::{ArtifactCapability, ArtifactTokenAuthority, ArtifactTokenError}; -pub use capability::{CapabilityAuthority, CapabilityClaims, CapabilityError}; -pub use projection::{ProjectionClient, ProjectionClientError, ProjectionRequestError}; -pub use session::NodeSessionServer; -pub use wire::checks_server::Checks as ChecksService; -pub use wire::checks_server::ChecksServer as GeneratedChecksServer; -pub use wire::node_session_client::NodeSessionClient; -pub use wire::node_session_server::NodeSessionServer as GeneratedServer; -pub use wire::runtime_secrets_server::RuntimeSecrets as RuntimeSecretsService; -pub use wire::runtime_secrets_server::RuntimeSecretsServer as GeneratedSecretsServer; - -pub const REPOSITORY_TOKEN_SECRET: &str = "SYNCODE_REPOSITORY_TOKEN"; +mod artifact; +mod capability; +mod declaration; +mod error; +mod identity; +mod outbound; +mod reports; +mod repository_coordinates; +mod session; + +pub mod wire { + //! Generated from `proto/node.proto`. + #![allow(clippy::doc_markdown, clippy::large_enum_variant, clippy::use_self)] + + include!(concat!(env!("OUT_DIR"), "/syncode.node.v1.rs")); +} + +pub mod identity_wire { + #![allow(clippy::doc_markdown, clippy::large_enum_variant, clippy::use_self)] + + include!(concat!(env!("OUT_DIR"), "/syncode.identity.v1.rs")); +} + +pub mod actions_wire { + #![allow(clippy::doc_markdown, clippy::large_enum_variant, clippy::use_self)] + + include!(concat!(env!("OUT_DIR"), "/syncode.control.v1.rs")); +} + +pub use actions_wire::actions_read_server::ActionsRead as ActionsReadService; +pub use actions_wire::actions_read_server::ActionsReadServer as GeneratedActionsReadServer; +pub use artifact::{ArtifactCapability, ArtifactTokenAuthority, ArtifactTokenError}; +pub use capability::{CapabilityAuthority, CapabilityClaims, CapabilityError}; +pub use repository_coordinates::{ + RepositoryCoordinateRequestError, RepositoryCoordinates, RepositoryCoordinatesError, +}; +pub use session::NodeSessionServer; +pub use wire::checks_server::Checks as ChecksService; +pub use wire::checks_server::ChecksServer as GeneratedChecksServer; +pub use wire::node_session_client::NodeSessionClient; +pub use wire::node_session_server::NodeSessionServer as GeneratedServer; +pub use wire::runtime_secrets_server::RuntimeSecrets as RuntimeSecretsService; +pub use wire::runtime_secrets_server::RuntimeSecretsServer as GeneratedSecretsServer; + +pub const REPOSITORY_TOKEN_SECRET: &str = "SYNCODE_REPOSITORY_TOKEN"; diff --git a/crates/control-node/src/session.rs b/crates/control-node/src/session.rs --- a/crates/control-node/src/session.rs +++ b/crates/control-node/src/session.rs @@ -1,243 +1,243 @@ -mod acknowledgement; -mod dispatch; -mod obsolete; -mod server; -mod state; - -use std::time::{Duration, SystemTime}; - -use prost::Message; -use syncode_control_nodes::{InboxDecision, NodeStore, Nodes}; -use syncode_control_runs::{RunLog, Runs}; -use tokio::sync::mpsc; -use tonic::Status; -use url::Url; - -use crate::error::SessionError; -use crate::identity::{enrol_node, greet, renew_credential}; -use crate::outbound::{Outbound, send}; -use crate::reports::{accept_logs, report}; -use crate::wire::{ControlMessage, NodeMessage, Welcome, control_message, node_message}; -use crate::{ArtifactTokenAuthority, CapabilityAuthority, ProjectionClient}; -use acknowledgement::{acknowledge_node_message, acknowledgement}; -use dispatch::{assignment_is_current, beat, holder, offer_work}; -use obsolete::obsolete_report; -use state::Session; - -const CHANNEL_DEPTH: usize = 32; -const OUTBOUND_RETRY_INTERVAL: Duration = Duration::from_secs(2); - -pub struct NodeSessionServer { - control: Runs, - nodes: Nodes, - authority: CapabilityAuthority, - artifact_authority: ArtifactTokenAuthority, - action_artifact_url: Url, - projection: ProjectionClient, - assignments: bool, -} - -struct Registries<'a, L, S> { - runs: &'a Runs, - nodes: &'a Nodes, - authority: &'a CapabilityAuthority, - artifact_authority: &'a ArtifactTokenAuthority, - action_artifact_url: &'a Url, - projection: &'a ProjectionClient, - assignments: bool, -} - -impl Clone for Registries<'_, L, S> { - fn clone(&self) -> Self { - *self - } -} - -impl Copy for Registries<'_, L, S> {} - -async fn serve( - registries: Registries<'_, L, S>, - outbound: &mut Outbound, - session: &mut Session, - message: NodeMessage, - sender: &mpsc::Sender>, -) -> Result<(), SessionError> { - let control = registries.runs; - if !session.accept(&message)? { - acknowledge_node_message(outbound, &message, sender).await?; - return Ok(()); - } - let tracked = matches!( - message.body.as_ref(), - Some( - node_message::Body::Progress(_) - | node_message::Body::Heartbeat(_) - | node_message::Body::RotationAccepted(_) - | node_message::Body::Logs(_) - ) - ); - let node = tracked.then(|| holder(session.node)).transpose()?; - if let Some(node) = node { - let payload = NodeMessage { - sequence: 0, - message_id: String::new(), - idempotency_key: String::new(), - body: message.body.clone(), - } - .encode_to_vec(); - match registries - .nodes - .message_started( - node, - &message.message_id, - &message.idempotency_key, - &payload, - ) - .await? - { - InboxDecision::New => {} - InboxDecision::Duplicate => { - acknowledge_node_message(outbound, &message, sender).await?; - return Ok(()); - } - InboxDecision::MessageIdConflict => { - return Err(SessionError::MessageIdConflict(message.message_id)); - } - InboxDecision::IdempotencyConflict => { - return Err(SessionError::IdempotencyConflict(message.idempotency_key)); - } - } - } - let acknowledgement = acknowledgement(&message); - let progress = matches!(message.body.as_ref(), Some(node_message::Body::Progress(_))); - let result = match message.body { - Some(node_message::Body::Enrol(enrol)) => { - enrol_node(registries.nodes, outbound, &enrol, sender).await - } - Some(node_message::Body::Hello(hello)) => { - let identity: syncode_control_runs::NodeId = hello.node.parse()?; - registries - .nodes - .authenticate(identity, &hello.credential, SystemTime::now()) - .await?; - if !registries.nodes.session_opened(identity).await { - return Err(SessionError::ConcurrentSession(identity)); - } - if let Err(error) = greet(registries.nodes, identity, &hello).await { - registries.nodes.session_abandoned(identity).await; - return Err(error); - } - session.node = Some(identity); - session.maximum = usize::try_from(hello.max_parallel) - .map_err(|_| SessionError::InvalidParallelism(hello.max_parallel))?; - if session.maximum == 0 { - return Err(SessionError::InvalidParallelism(hello.max_parallel)); - } - session.available = session - .maximum - .saturating_sub(control.leases_held_by(identity).await); - session.work_version = control.work_version(); - let welcome = control_message::Body::Welcome(Welcome { - node: identity.to_string(), - }); - send(sender, outbound.next(welcome)).await?; - let mut replayed_rotation = false; - for pending in registries.nodes.outbound_pending(identity).await? { - let stored = ControlMessage::decode(pending.payload.as_slice()) - .map_err(SessionError::Outbox)?; - if !assignment_is_current(control, identity, &stored).await? { - registries - .nodes - .outbound_acknowledged(identity, &pending.message_id) - .await?; - continue; - } - replayed_rotation |= matches!(stored.body, Some(control_message::Body::Rotated(_))); - let replayed = outbound.resequence(stored); - send(sender, replayed.clone()).await?; - session.track(replayed); - } - // A node reconnecting late in the life of its credential is handed - // the next one before it is handed work, so a long job never - // outlives the credential that has to report it. - if !replayed_rotation - && let Some(rotation) = - renew_credential(registries.nodes, outbound, identity, sender).await? - { - session.track(rotation); - } - offer_work(registries, outbound, session, sender).await - } - Some(node_message::Body::Progress(progress)) => { - let identity = holder(session.node)?; - if report(control, identity, progress).await? { - session.release(); - offer_work(registries, outbound, session, sender).await?; - } - Ok(()) - } - Some(node_message::Body::Heartbeat(heartbeat)) => { - let identity = holder(session.node)?; - beat(registries, identity, &heartbeat).await?; - if let Some(rotation) = - renew_credential(registries.nodes, outbound, identity, sender).await? - { - session.track(rotation); - } - offer_work(registries, outbound, session, sender).await - } - Some(node_message::Body::RotationAccepted(accepted)) => { - let identity = holder(session.node)?; - registries - .nodes - .accept_rotation(identity, &accepted.credential) - .await?; - Ok(()) - } - Some(node_message::Body::Logs(logs)) => { - let identity = holder(session.node)?; - accept_logs(control, identity, &logs).await - } - Some(node_message::Body::Acknowledgement(acknowledgement)) => { - let identity = holder(session.node)?; - registries - .nodes - .outbound_acknowledged(identity, &acknowledgement.message_id) - .await?; - session.acknowledged(&acknowledgement.message_id); - Ok(()) - } - None => Err(SessionError::EmptyMessage), - }; - let obsolete = result.as_ref().err().is_some_and(obsolete_report); - if let Some(node) = node { - registries - .nodes - .message_finished(node, &message.message_id, result.is_ok() || obsolete) - .await?; - } - if obsolete { - if let Some(mut acknowledgement) = acknowledgement { - if progress { - acknowledgement.conclusion = crate::wire::Conclusion::Cancelled as i32; - } - send( - sender, - outbound.next(control_message::Body::Acknowledgement(acknowledgement)), - ) - .await?; - } - offer_work(registries, outbound, session, sender).await?; - return Ok(()); - } - if result.is_ok() - && let Some(acknowledgement) = acknowledgement - { - send( - sender, - outbound.next(control_message::Body::Acknowledgement(acknowledgement)), - ) - .await?; - } - result -} +mod acknowledgement; +mod dispatch; +mod obsolete; +mod server; +mod state; + +use std::time::{Duration, SystemTime}; + +use prost::Message; +use syncode_control_nodes::{InboxDecision, NodeStore, Nodes}; +use syncode_control_runs::{RunLog, Runs}; +use tokio::sync::mpsc; +use tonic::Status; +use url::Url; + +use crate::error::SessionError; +use crate::identity::{enrol_node, greet, renew_credential}; +use crate::outbound::{Outbound, send}; +use crate::reports::{accept_logs, report}; +use crate::wire::{ControlMessage, NodeMessage, Welcome, control_message, node_message}; +use crate::{ArtifactTokenAuthority, CapabilityAuthority, RepositoryCoordinates}; +use acknowledgement::{acknowledge_node_message, acknowledgement}; +use dispatch::{assignment_is_current, beat, holder, offer_work}; +use obsolete::obsolete_report; +use state::Session; + +const CHANNEL_DEPTH: usize = 32; +const OUTBOUND_RETRY_INTERVAL: Duration = Duration::from_secs(2); + +pub struct NodeSessionServer { + control: Runs, + nodes: Nodes, + authority: CapabilityAuthority, + artifact_authority: ArtifactTokenAuthority, + action_artifact_url: Url, + repositories: RepositoryCoordinates, + assignments: bool, +} + +struct Registries<'a, L, S> { + runs: &'a Runs, + nodes: &'a Nodes, + authority: &'a CapabilityAuthority, + artifact_authority: &'a ArtifactTokenAuthority, + action_artifact_url: &'a Url, + repositories: &'a RepositoryCoordinates, + assignments: bool, +} + +impl Clone for Registries<'_, L, S> { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for Registries<'_, L, S> {} + +async fn serve( + registries: Registries<'_, L, S>, + outbound: &mut Outbound, + session: &mut Session, + message: NodeMessage, + sender: &mpsc::Sender>, +) -> Result<(), SessionError> { + let control = registries.runs; + if !session.accept(&message)? { + acknowledge_node_message(outbound, &message, sender).await?; + return Ok(()); + } + let tracked = matches!( + message.body.as_ref(), + Some( + node_message::Body::Progress(_) + | node_message::Body::Heartbeat(_) + | node_message::Body::RotationAccepted(_) + | node_message::Body::Logs(_) + ) + ); + let node = tracked.then(|| holder(session.node)).transpose()?; + if let Some(node) = node { + let payload = NodeMessage { + sequence: 0, + message_id: String::new(), + idempotency_key: String::new(), + body: message.body.clone(), + } + .encode_to_vec(); + match registries + .nodes + .message_started( + node, + &message.message_id, + &message.idempotency_key, + &payload, + ) + .await? + { + InboxDecision::New => {} + InboxDecision::Duplicate => { + acknowledge_node_message(outbound, &message, sender).await?; + return Ok(()); + } + InboxDecision::MessageIdConflict => { + return Err(SessionError::MessageIdConflict(message.message_id)); + } + InboxDecision::IdempotencyConflict => { + return Err(SessionError::IdempotencyConflict(message.idempotency_key)); + } + } + } + let acknowledgement = acknowledgement(&message); + let progress = matches!(message.body.as_ref(), Some(node_message::Body::Progress(_))); + let result = match message.body { + Some(node_message::Body::Enrol(enrol)) => { + enrol_node(registries.nodes, outbound, &enrol, sender).await + } + Some(node_message::Body::Hello(hello)) => { + let identity: syncode_control_runs::NodeId = hello.node.parse()?; + registries + .nodes + .authenticate(identity, &hello.credential, SystemTime::now()) + .await?; + if !registries.nodes.session_opened(identity).await { + return Err(SessionError::ConcurrentSession(identity)); + } + if let Err(error) = greet(registries.nodes, identity, &hello).await { + registries.nodes.session_abandoned(identity).await; + return Err(error); + } + session.node = Some(identity); + session.maximum = usize::try_from(hello.max_parallel) + .map_err(|_| SessionError::InvalidParallelism(hello.max_parallel))?; + if session.maximum == 0 { + return Err(SessionError::InvalidParallelism(hello.max_parallel)); + } + session.available = session + .maximum + .saturating_sub(control.leases_held_by(identity).await); + session.work_version = control.work_version(); + let welcome = control_message::Body::Welcome(Welcome { + node: identity.to_string(), + }); + send(sender, outbound.next(welcome)).await?; + let mut replayed_rotation = false; + for pending in registries.nodes.outbound_pending(identity).await? { + let stored = ControlMessage::decode(pending.payload.as_slice()) + .map_err(SessionError::Outbox)?; + if !assignment_is_current(control, identity, &stored).await? { + registries + .nodes + .outbound_acknowledged(identity, &pending.message_id) + .await?; + continue; + } + replayed_rotation |= matches!(stored.body, Some(control_message::Body::Rotated(_))); + let replayed = outbound.resequence(stored); + send(sender, replayed.clone()).await?; + session.track(replayed); + } + // A node reconnecting late in the life of its credential is handed + // the next one before it is handed work, so a long job never + // outlives the credential that has to report it. + if !replayed_rotation + && let Some(rotation) = + renew_credential(registries.nodes, outbound, identity, sender).await? + { + session.track(rotation); + } + offer_work(registries, outbound, session, sender).await + } + Some(node_message::Body::Progress(progress)) => { + let identity = holder(session.node)?; + if report(control, identity, progress).await? { + session.release(); + offer_work(registries, outbound, session, sender).await?; + } + Ok(()) + } + Some(node_message::Body::Heartbeat(heartbeat)) => { + let identity = holder(session.node)?; + beat(registries, identity, &heartbeat).await?; + if let Some(rotation) = + renew_credential(registries.nodes, outbound, identity, sender).await? + { + session.track(rotation); + } + offer_work(registries, outbound, session, sender).await + } + Some(node_message::Body::RotationAccepted(accepted)) => { + let identity = holder(session.node)?; + registries + .nodes + .accept_rotation(identity, &accepted.credential) + .await?; + Ok(()) + } + Some(node_message::Body::Logs(logs)) => { + let identity = holder(session.node)?; + accept_logs(control, identity, &logs).await + } + Some(node_message::Body::Acknowledgement(acknowledgement)) => { + let identity = holder(session.node)?; + registries + .nodes + .outbound_acknowledged(identity, &acknowledgement.message_id) + .await?; + session.acknowledged(&acknowledgement.message_id); + Ok(()) + } + None => Err(SessionError::EmptyMessage), + }; + let obsolete = result.as_ref().err().is_some_and(obsolete_report); + if let Some(node) = node { + registries + .nodes + .message_finished(node, &message.message_id, result.is_ok() || obsolete) + .await?; + } + if obsolete { + if let Some(mut acknowledgement) = acknowledgement { + if progress { + acknowledgement.conclusion = crate::wire::Conclusion::Cancelled as i32; + } + send( + sender, + outbound.next(control_message::Body::Acknowledgement(acknowledgement)), + ) + .await?; + } + offer_work(registries, outbound, session, sender).await?; + return Ok(()); + } + if result.is_ok() + && let Some(acknowledgement) = acknowledgement + { + send( + sender, + outbound.next(control_message::Body::Acknowledgement(acknowledgement)), + ) + .await?; + } + result +} diff --git a/crates/control-node/src/session/dispatch.rs b/crates/control-node/src/session/dispatch.rs --- a/crates/control-node/src/session/dispatch.rs +++ b/crates/control-node/src/session/dispatch.rs @@ -1,211 +1,196 @@ -mod assignment; - -use std::time::SystemTime; - -use prost::Message; -use syncode_control_nodes::{NodeStore, NodesError, Refusal}; -use syncode_control_runs::{Dispatch, Fence, NodeId, RunId, RunLog, Runs}; -use tokio::sync::mpsc; -use tonic::Status; - -use super::Registries; -use super::state::Session; -use crate::declaration; -use crate::error::SessionError; -use crate::outbound::{DurableDelivery, Outbound, send, send_durable}; -use crate::projection::ProjectionClient; -use crate::wire::{ControlMessage, Heartbeat, Refused, control_message}; -use assignment::assignment_body; - -pub(super) async fn offer_work( - registries: Registries<'_, L, S>, - outbound: &mut Outbound, - session: &mut Session, - sender: &mpsc::Sender>, -) -> Result<(), SessionError> { - if !registries.assignments { - return Ok(()); - } - let node = holder(session.node)?; - offer_cancellations(registries, outbound, session, sender, node).await?; - session.available = session - .maximum - .saturating_sub(registries.runs.leases_held_by(node).await); - while session.available > 0 { - registries - .nodes - .session_available(node, session.available) - .await; - let candidate = match registries.nodes.scheduling_candidate(node).await { - Ok(candidate) => candidate, - Err(NodesError::Refused(Refusal::Undeclared(_))) => { - session.available = 0; - registries.nodes.session_available(node, 0).await; - return Ok(()); - } - Err(NodesError::Refused(refusal)) => { - let body = control_message::Body::Refused(Refused { - reason: refusal.to_string(), - }); - return send(sender, outbound.next(body)).await; - } - Err(error) => return Err(error.into()), - }; - let candidates = registries.nodes.available_candidates().await; - let assignment = match registries - .runs - .take_next_among(&candidate, &candidates) - .await? - { - Dispatch::Assigned(assignment) => assignment, - Dispatch::Empty => break, - Dispatch::Refused(_) => break, - }; - retire_previous_assignments(registries.nodes, node, session, &assignment).await?; - project_before_dispatch(registries.runs, registries.projection, assignment.run()).await?; - let repository = registries - .projection - .repository_coordinates(assignment.origin().repository()) - .await?; - let body = assignment_body( - &assignment, - repository, - node, - registries.authority, - registries.artifact_authority, - registries.action_artifact_url, - )?; - match send_durable(registries.nodes, node, sender, outbound, body).await? { - DurableDelivery::Pending(sent) => session.track(*sent), - DurableDelivery::Acknowledged => {} - } - session.available -= 1; - } - registries - .nodes - .session_available(node, session.available) - .await; - Ok(()) -} - -async fn retire_previous_assignments( - nodes: &syncode_control_nodes::Nodes, - node: NodeId, - session: &mut Session, - assignment: &syncode_control_runs::Assignment, -) -> Result<(), SessionError> { - for pending in nodes.outbound_pending(node).await? { - let stored = - ControlMessage::decode(pending.payload.as_slice()).map_err(SessionError::Outbox)?; - let same_job = matches!( - stored.body, - Some(control_message::Body::Assignment(ref previous)) - if previous.run == assignment.run().to_string() - && previous.job == assignment.job().to_string() - && previous.fence != assignment.fence().get() - ); - if same_job { - nodes - .outbound_acknowledged(node, &pending.message_id) - .await?; - session.acknowledged(&pending.message_id); - } - } - Ok(()) -} - -pub(super) async fn assignment_is_current( - runs: &syncode_control_runs::Runs, - node: NodeId, - message: &ControlMessage, -) -> Result { - let Some(control_message::Body::Assignment(assignment)) = message.body.as_ref() else { - return Ok(true); - }; - Ok(runs - .assignment_is_current( - assignment.run.parse()?, - assignment.job.parse()?, - node, - Fence::from(assignment.fence), - SystemTime::now(), - ) - .await) -} - -async fn offer_cancellations( - registries: Registries<'_, L, S>, - outbound: &mut Outbound, - session: &mut Session, - sender: &mpsc::Sender>, - node: NodeId, -) -> Result<(), SessionError> { - for cancellation in registries.runs.pending_cancellations(node).await { - let body = control_message::Body::Cancel(crate::wire::Cancel { - run: cancellation.run().to_string(), - fence: cancellation.fence().get(), - job: cancellation.job().to_string(), - }); - if let DurableDelivery::Pending(sent) = - send_durable(registries.nodes, node, sender, outbound, body).await? - { - session.track(*sent); - } - } - Ok(()) -} - -async fn project_before_dispatch( - runs: &Runs, - projection: &ProjectionClient, - run: RunId, -) -> Result<(), SessionError> { - let projected = runs - .projection(run) - .await? - .ok_or(SessionError::MissingProjection(run))?; - projection.send(&projected).await?; - Ok(()) -} - -pub(super) async fn retry_pending( - session: &Session, - sender: &mpsc::Sender>, -) -> Result<(), SessionError> { - for message in session.pending() { - send(sender, message.clone()).await?; - } - Ok(()) -} - -pub(super) fn holder(node: Option) -> Result { - node.ok_or(SessionError::NoHello) -} - -pub(super) async fn beat( - registries: Registries<'_, L, S>, - node: NodeId, - heartbeat: &Heartbeat, -) -> Result<(), SessionError> { - let capacity = heartbeat - .capacity - .as_ref() - .map(declaration::capacity) - .transpose()?; - registries - .nodes - .beat(node, capacity, SystemTime::now()) - .await?; - for held in &heartbeat.held { - registries - .runs - .renewed_job( - held.run.parse()?, - held.job.parse()?, - node, - Fence::from(held.fence), - ) - .await?; - } - Ok(()) -} +mod assignment; + +use std::time::SystemTime; + +use prost::Message; +use syncode_control_nodes::{NodeStore, NodesError, Refusal}; +use syncode_control_runs::{Dispatch, Fence, NodeId, RunLog}; +use tokio::sync::mpsc; +use tonic::Status; + +use super::Registries; +use super::state::Session; +use crate::declaration; +use crate::error::SessionError; +use crate::outbound::{DurableDelivery, Outbound, send, send_durable}; +use crate::wire::{ControlMessage, Heartbeat, Refused, control_message}; +use assignment::assignment_body; + +pub(super) async fn offer_work( + registries: Registries<'_, L, S>, + outbound: &mut Outbound, + session: &mut Session, + sender: &mpsc::Sender>, +) -> Result<(), SessionError> { + if !registries.assignments { + return Ok(()); + } + let node = holder(session.node)?; + offer_cancellations(registries, outbound, session, sender, node).await?; + session.available = session + .maximum + .saturating_sub(registries.runs.leases_held_by(node).await); + while session.available > 0 { + registries + .nodes + .session_available(node, session.available) + .await; + let candidate = match registries.nodes.scheduling_candidate(node).await { + Ok(candidate) => candidate, + Err(NodesError::Refused(Refusal::Undeclared(_))) => { + session.available = 0; + registries.nodes.session_available(node, 0).await; + return Ok(()); + } + Err(NodesError::Refused(refusal)) => { + let body = control_message::Body::Refused(Refused { + reason: refusal.to_string(), + }); + return send(sender, outbound.next(body)).await; + } + Err(error) => return Err(error.into()), + }; + let candidates = registries.nodes.available_candidates().await; + let assignment = match registries + .runs + .take_next_among(&candidate, &candidates) + .await? + { + Dispatch::Assigned(assignment) => assignment, + Dispatch::Empty => break, + Dispatch::Refused(_) => break, + }; + retire_previous_assignments(registries.nodes, node, session, &assignment).await?; + let repository = registries + .repositories + .resolve(assignment.origin().repository()) + .await?; + let body = assignment_body( + &assignment, + repository, + node, + registries.authority, + registries.artifact_authority, + registries.action_artifact_url, + )?; + match send_durable(registries.nodes, node, sender, outbound, body).await? { + DurableDelivery::Pending(sent) => session.track(*sent), + DurableDelivery::Acknowledged => {} + } + session.available -= 1; + } + registries + .nodes + .session_available(node, session.available) + .await; + Ok(()) +} + +async fn retire_previous_assignments( + nodes: &syncode_control_nodes::Nodes, + node: NodeId, + session: &mut Session, + assignment: &syncode_control_runs::Assignment, +) -> Result<(), SessionError> { + for pending in nodes.outbound_pending(node).await? { + let stored = + ControlMessage::decode(pending.payload.as_slice()).map_err(SessionError::Outbox)?; + let same_job = matches!( + stored.body, + Some(control_message::Body::Assignment(ref previous)) + if previous.run == assignment.run().to_string() + && previous.job == assignment.job().to_string() + && previous.fence != assignment.fence().get() + ); + if same_job { + nodes + .outbound_acknowledged(node, &pending.message_id) + .await?; + session.acknowledged(&pending.message_id); + } + } + Ok(()) +} + +pub(super) async fn assignment_is_current( + runs: &syncode_control_runs::Runs, + node: NodeId, + message: &ControlMessage, +) -> Result { + let Some(control_message::Body::Assignment(assignment)) = message.body.as_ref() else { + return Ok(true); + }; + Ok(runs + .assignment_is_current( + assignment.run.parse()?, + assignment.job.parse()?, + node, + Fence::from(assignment.fence), + SystemTime::now(), + ) + .await) +} + +async fn offer_cancellations( + registries: Registries<'_, L, S>, + outbound: &mut Outbound, + session: &mut Session, + sender: &mpsc::Sender>, + node: NodeId, +) -> Result<(), SessionError> { + for cancellation in registries.runs.pending_cancellations(node).await { + let body = control_message::Body::Cancel(crate::wire::Cancel { + run: cancellation.run().to_string(), + fence: cancellation.fence().get(), + job: cancellation.job().to_string(), + }); + if let DurableDelivery::Pending(sent) = + send_durable(registries.nodes, node, sender, outbound, body).await? + { + session.track(*sent); + } + } + Ok(()) +} + +pub(super) async fn retry_pending( + session: &Session, + sender: &mpsc::Sender>, +) -> Result<(), SessionError> { + for message in session.pending() { + send(sender, message.clone()).await?; + } + Ok(()) +} + +pub(super) fn holder(node: Option) -> Result { + node.ok_or(SessionError::NoHello) +} + +pub(super) async fn beat( + registries: Registries<'_, L, S>, + node: NodeId, + heartbeat: &Heartbeat, +) -> Result<(), SessionError> { + let capacity = heartbeat + .capacity + .as_ref() + .map(declaration::capacity) + .transpose()?; + registries + .nodes + .beat(node, capacity, SystemTime::now()) + .await?; + for held in &heartbeat.held { + registries + .runs + .renewed_job( + held.run.parse()?, + held.job.parse()?, + node, + Fence::from(held.fence), + ) + .await?; + } + Ok(()) +} diff --git a/crates/control-node/src/session/server.rs b/crates/control-node/src/session/server.rs --- a/crates/control-node/src/session/server.rs +++ b/crates/control-node/src/session/server.rs @@ -1,154 +1,154 @@ -use std::pin::Pin; - -use syncode_control_nodes::{NodeStore, Nodes}; -use syncode_control_runs::{RunLog, Runs}; -use tokio::sync::mpsc; -use tokio_stream::wrappers::ReceiverStream; -use tokio_stream::{Stream, StreamExt}; -use tonic::{Request, Response, Status, Streaming}; - -use super::dispatch::{offer_work, retry_pending}; -use super::state::Session; -use super::{CHANNEL_DEPTH, NodeSessionServer, OUTBOUND_RETRY_INTERVAL, Registries, serve}; -use crate::error::SessionError; -use crate::outbound::Outbound; -use crate::wire::node_session_server::NodeSession; -use crate::wire::{ControlMessage, NodeMessage}; -use crate::{ArtifactTokenAuthority, CapabilityAuthority}; - -#[cfg(test)] -mod tests; - -// A derive would add `L: Clone, S: Clone` bounds neither `Runs` nor -// `Nodes` need — both clone by sharing their own internal handle. -impl Clone for NodeSessionServer { - fn clone(&self) -> Self { - Self { - control: self.control.clone(), - nodes: self.nodes.clone(), - authority: self.authority.clone(), - artifact_authority: self.artifact_authority.clone(), - action_artifact_url: self.action_artifact_url.clone(), - projection: self.projection.clone(), - assignments: self.assignments, - } - } -} - -impl NodeSessionServer { - #[must_use] - pub const fn new( - control: Runs, - nodes: Nodes, - authority: CapabilityAuthority, - artifact_authority: ArtifactTokenAuthority, - action_artifact_url: url::Url, - projection: crate::ProjectionClient, - ) -> Self { - Self { - control, - nodes, - authority, - artifact_authority, - action_artifact_url, - projection, - assignments: true, - } - } - - #[must_use] - pub const fn shadow( - control: Runs, - nodes: Nodes, - authority: CapabilityAuthority, - artifact_authority: ArtifactTokenAuthority, - action_artifact_url: url::Url, - projection: crate::ProjectionClient, - ) -> Self { - Self { - control, - nodes, - authority, - artifact_authority, - action_artifact_url, - projection, - assignments: false, - } - } -} - -#[tonic::async_trait] -impl NodeSession for NodeSessionServer { - type OpenStream = Pin> + Send>>; - - async fn open( - &self, - request: Request>, - ) -> Result, Status> { - let mut inbound = request.into_inner(); - let (sender, receiver) = mpsc::channel(CHANNEL_DEPTH); - let server = self.clone(); - - tokio::spawn(async move { - if let Err(error) = run_session(&mut inbound, &sender, server).await { - eprintln!("syncode control: node session failed: {error}"); - } - }); - - Ok(Response::new(Box::pin(ReceiverStream::new(receiver)))) - } -} - -async fn run_session( - inbound: &mut I, - sender: &mpsc::Sender>, - server: NodeSessionServer, -) -> Result<(), SessionError> -where - I: Stream> + Unpin, -{ - let mut outbound = Outbound::default(); - let mut session = Session::default(); - let registries = Registries { - runs: &server.control, - nodes: &server.nodes, - authority: &server.authority, - artifact_authority: &server.artifact_authority, - action_artifact_url: &server.action_artifact_url, - projection: &server.projection, - assignments: server.assignments, - }; - let mut retry = tokio::time::interval(OUTBOUND_RETRY_INTERVAL); - retry.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); - retry.tick().await; - loop { - let work_version = session.work_version; - let served = tokio::select! { - message = inbound.next() => match message { - Some(Ok(message)) => serve( - registries, &mut outbound, &mut session, message, sender, - ).await, - Some(Err(error)) => Err(SessionError::Transport(error)), - None => break, - }, - version = server.control.wait_for_work(work_version), if session.is_open() => { - session.work_version = version; - offer_work(registries, &mut outbound, &mut session, sender).await - } - _ = retry.tick(), if session.has_pending() => retry_pending(&session, sender).await - }; - if let Err(error) = served { - let _ = sender.send(Err(error.into())).await; - break; - } - } - if let Some(id) = session.node - && let Err(error) = server.nodes.session_closed(id).await - { - sender - .send(Err(SessionError::from(error).into())) - .await - .map_err(|_| SessionError::Unreadable)?; - } - Ok(()) -} +use std::pin::Pin; + +use syncode_control_nodes::{NodeStore, Nodes}; +use syncode_control_runs::{RunLog, Runs}; +use tokio::sync::mpsc; +use tokio_stream::wrappers::ReceiverStream; +use tokio_stream::{Stream, StreamExt}; +use tonic::{Request, Response, Status, Streaming}; + +use super::dispatch::{offer_work, retry_pending}; +use super::state::Session; +use super::{CHANNEL_DEPTH, NodeSessionServer, OUTBOUND_RETRY_INTERVAL, Registries, serve}; +use crate::error::SessionError; +use crate::outbound::Outbound; +use crate::wire::node_session_server::NodeSession; +use crate::wire::{ControlMessage, NodeMessage}; +use crate::{ArtifactTokenAuthority, CapabilityAuthority}; + +#[cfg(test)] +mod tests; + +// A derive would add `L: Clone, S: Clone` bounds neither `Runs` nor +// `Nodes` need — both clone by sharing their own internal handle. +impl Clone for NodeSessionServer { + fn clone(&self) -> Self { + Self { + control: self.control.clone(), + nodes: self.nodes.clone(), + authority: self.authority.clone(), + artifact_authority: self.artifact_authority.clone(), + action_artifact_url: self.action_artifact_url.clone(), + repositories: self.repositories.clone(), + assignments: self.assignments, + } + } +} + +impl NodeSessionServer { + #[must_use] + pub const fn new( + control: Runs, + nodes: Nodes, + authority: CapabilityAuthority, + artifact_authority: ArtifactTokenAuthority, + action_artifact_url: url::Url, + repositories: crate::RepositoryCoordinates, + ) -> Self { + Self { + control, + nodes, + authority, + artifact_authority, + action_artifact_url, + repositories, + assignments: true, + } + } + + #[must_use] + pub const fn shadow( + control: Runs, + nodes: Nodes, + authority: CapabilityAuthority, + artifact_authority: ArtifactTokenAuthority, + action_artifact_url: url::Url, + repositories: crate::RepositoryCoordinates, + ) -> Self { + Self { + control, + nodes, + authority, + artifact_authority, + action_artifact_url, + repositories, + assignments: false, + } + } +} + +#[tonic::async_trait] +impl NodeSession for NodeSessionServer { + type OpenStream = Pin> + Send>>; + + async fn open( + &self, + request: Request>, + ) -> Result, Status> { + let mut inbound = request.into_inner(); + let (sender, receiver) = mpsc::channel(CHANNEL_DEPTH); + let server = self.clone(); + + tokio::spawn(async move { + if let Err(error) = run_session(&mut inbound, &sender, server).await { + eprintln!("syncode control: node session failed: {error}"); + } + }); + + Ok(Response::new(Box::pin(ReceiverStream::new(receiver)))) + } +} + +async fn run_session( + inbound: &mut I, + sender: &mpsc::Sender>, + server: NodeSessionServer, +) -> Result<(), SessionError> +where + I: Stream> + Unpin, +{ + let mut outbound = Outbound::default(); + let mut session = Session::default(); + let registries = Registries { + runs: &server.control, + nodes: &server.nodes, + authority: &server.authority, + artifact_authority: &server.artifact_authority, + action_artifact_url: &server.action_artifact_url, + repositories: &server.repositories, + assignments: server.assignments, + }; + let mut retry = tokio::time::interval(OUTBOUND_RETRY_INTERVAL); + retry.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); + retry.tick().await; + loop { + let work_version = session.work_version; + let served = tokio::select! { + message = inbound.next() => match message { + Some(Ok(message)) => serve( + registries, &mut outbound, &mut session, message, sender, + ).await, + Some(Err(error)) => Err(SessionError::Transport(error)), + None => break, + }, + version = server.control.wait_for_work(work_version), if session.is_open() => { + session.work_version = version; + offer_work(registries, &mut outbound, &mut session, sender).await + } + _ = retry.tick(), if session.has_pending() => retry_pending(&session, sender).await + }; + if let Err(error) = served { + let _ = sender.send(Err(error.into())).await; + break; + } + } + if let Some(id) = session.node + && let Err(error) = server.nodes.session_closed(id).await + { + sender + .send(Err(SessionError::from(error).into())) + .await + .map_err(|_| SessionError::Unreadable)?; + } + Ok(()) +} diff --git a/crates/control-runs/src/audit/payload.rs b/crates/control-runs/src/audit/payload.rs --- a/crates/control-runs/src/audit/payload.rs +++ b/crates/control-runs/src/audit/payload.rs @@ -1,126 +1,125 @@ -use std::time::SystemTime; - -use serde::{Deserialize, Serialize}; - -use crate::{ - Architecture, JobId, MatrixPolicy, NodeId, OperatingSystem, Origin, Priority, Requirements, - RunEvent, SchedulingRefusal, -}; - -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -#[serde(untagged)] -pub enum AuditPayload { - Origin(Origin), - Jobs(AuditJobs), - Enrolment(AuditEnrolment), - Node(AuditNodeSnapshot), - RunEvent(RunEvent), - Configuration(AuditConfiguration), - RunAdmission(AuditRunAdmission), - SchedulingRefusals(Vec), -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct AuditJobs { - pub jobs: Vec, -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub struct AuditJob { - pub id: JobId, - pub key: String, - pub needs: Vec, - pub matrix: MatrixPolicy, - pub priority: Priority, - pub requirements: Requirements, - pub secrets: Vec, -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub struct AuditEnrolment { - pub scope: AuditNodeScope, -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub enum AuditNodeScope { - Instance, - Organisation(String), - Repository(String), -} - -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub enum AuditNodeLifecycle { - Enrolled, - Active, - Draining, - Offline, - Revoked, -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub struct AuditContainerRuntime { - pub name: String, - pub version: String, -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub struct AuditNodeCapabilities { - pub architecture: Architecture, - pub operating_system: OperatingSystem, - pub container_runtime: AuditContainerRuntime, - pub cores: u32, - pub memory_bytes: u64, - pub labels: Vec, -} - -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub struct AuditVolume { - pub total_bytes: u64, - pub used_bytes: u64, -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub struct AuditNodeCapacity { - pub build_volume_free_bytes: u64, - pub cache_volume: Option, - pub cache_volume_path: Option, - pub layer_store_bytes: u64, - pub cached_images: Vec, - pub cached_actions: Vec, -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub struct AuditNodeSnapshot { - pub id: NodeId, - pub scope: AuditNodeScope, - pub lifecycle: AuditNodeLifecycle, - pub capabilities: Option, - pub capacity: Option, - pub last_seen: SystemTime, - pub credential_expires_at: SystemTime, - pub rotation_pending: bool, -} - -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub enum AuditControlMode { - Shadow, - Active, -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub struct AuditConfiguration { - pub mode: AuditControlMode, - pub organization_concurrency: u32, - pub repository_concurrency: u32, - pub principal_concurrency: u32, - pub organization_queue_quota: u32, - pub principal_queue_quota: u32, - pub audit_retention_days: u32, - pub projection_min_run_number: u64, -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub struct AuditRunAdmission { - pub repository: String, - pub jobs: usize, -} +use std::time::SystemTime; + +use serde::{Deserialize, Serialize}; + +use crate::{ + Architecture, JobId, MatrixPolicy, NodeId, OperatingSystem, Origin, Priority, Requirements, + RunEvent, SchedulingRefusal, +}; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum AuditPayload { + Origin(Origin), + Jobs(AuditJobs), + Enrolment(AuditEnrolment), + Node(AuditNodeSnapshot), + RunEvent(RunEvent), + Configuration(AuditConfiguration), + RunAdmission(AuditRunAdmission), + SchedulingRefusals(Vec), +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct AuditJobs { + pub jobs: Vec, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct AuditJob { + pub id: JobId, + pub key: String, + pub needs: Vec, + pub matrix: MatrixPolicy, + pub priority: Priority, + pub requirements: Requirements, + pub secrets: Vec, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct AuditEnrolment { + pub scope: AuditNodeScope, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum AuditNodeScope { + Instance, + Organisation(String), + Repository(String), +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum AuditNodeLifecycle { + Enrolled, + Active, + Draining, + Offline, + Revoked, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct AuditContainerRuntime { + pub name: String, + pub version: String, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct AuditNodeCapabilities { + pub architecture: Architecture, + pub operating_system: OperatingSystem, + pub container_runtime: AuditContainerRuntime, + pub cores: u32, + pub memory_bytes: u64, + pub labels: Vec, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct AuditVolume { + pub total_bytes: u64, + pub used_bytes: u64, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct AuditNodeCapacity { + pub build_volume_free_bytes: u64, + pub cache_volume: Option, + pub cache_volume_path: Option, + pub layer_store_bytes: u64, + pub cached_images: Vec, + pub cached_actions: Vec, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct AuditNodeSnapshot { + pub id: NodeId, + pub scope: AuditNodeScope, + pub lifecycle: AuditNodeLifecycle, + pub capabilities: Option, + pub capacity: Option, + pub last_seen: SystemTime, + pub credential_expires_at: SystemTime, + pub rotation_pending: bool, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum AuditControlMode { + Shadow, + Active, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct AuditConfiguration { + pub mode: AuditControlMode, + pub organization_concurrency: u32, + pub repository_concurrency: u32, + pub principal_concurrency: u32, + pub organization_queue_quota: u32, + pub principal_queue_quota: u32, + pub audit_retention_days: u32, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct AuditRunAdmission { + pub repository: String, + pub jobs: usize, +} diff --git a/crates/control-node/src/session/server/tests.rs b/crates/control-node/src/session/server/tests.rs --- a/crates/control-node/src/session/server/tests.rs +++ b/crates/control-node/src/session/server/tests.rs @@ -1,141 +1,136 @@ -#![allow(clippy::expect_used, clippy::panic)] - -use std::time::Duration; - -use syncode_control_nodes::{Ephemeral, Lifecycle, Nodes, Scope}; -use syncode_control_runs::{Forgotten, Runs}; -use tokio::sync::mpsc; -use tokio_stream::wrappers::ReceiverStream; - -use super::{NodeSessionServer, run_session}; -use crate::wire::{ - Capabilities, Capacity, Enrol, Heartbeat, Hello, NodeMessage, control_message, node_message, -}; -use crate::{ArtifactTokenAuthority, CapabilityAuthority, ProjectionClient}; - -#[tokio::test] -async fn an_unreadable_client_releases_its_session() { - let runs = Runs::restored(Forgotten::default()).await.expect("runs"); - let nodes = Nodes::restored(Ephemeral::default()).await.expect("nodes"); - let token = nodes - .issue_token(Scope::Instance) - .await - .expect("enrolment token"); - let server = NodeSessionServer::new( - runs, - nodes.clone(), - CapabilityAuthority::new("test-capability-key").expect("capability authority"), - ArtifactTokenAuthority::new("test-artifact-key").expect("artifact authority"), - "http://127.0.0.1:1/".parse().expect("artifact URL"), - ProjectionClient::new( - "http://127.0.0.1:1/".parse().expect("projection URL"), - String::new(), - "http://127.0.0.1:1".to_owned(), - String::new(), - ) - .expect("projection client"), - ); - let (input, input_receiver) = mpsc::channel(8); - let (output, mut output_receiver) = mpsc::channel(8); - let session = tokio::spawn(async move { - let mut inbound = ReceiverStream::new(input_receiver); - run_session(&mut inbound, &output, server).await - }); - - input - .send(Ok(message( - 1, - node_message::Body::Enrol(Enrol { - token: token.secret().expose().to_owned(), - }), - ))) - .await - .expect("enrol"); - let enrolled = output_receiver - .recv() - .await - .expect("enrolment response") - .expect("enrolment status"); - let control_message::Body::Enrolled(enrolled) = enrolled.body.expect("enrolment body") else { - panic!("expected enrolment"); - }; - let node = enrolled.node.parse().expect("node id"); - - input - .send(Ok(message( - 2, - node_message::Body::Hello(Hello { - node: enrolled.node, - credential: enrolled.credential, - capabilities: Some(capabilities()), - capacity: Some(capacity()), - max_parallel: 1, - }), - ))) - .await - .expect("hello"); - let welcome = output_receiver - .recv() - .await - .expect("welcome response") - .expect("welcome status"); - assert!(matches!( - welcome.body, - Some(control_message::Body::Welcome(_)) - )); - drop(output_receiver); - - input - .send(Ok(message( - 3, - node_message::Body::Heartbeat(Heartbeat { - held: Vec::new(), - capacity: Some(capacity()), - }), - ))) - .await - .expect("heartbeat"); - tokio::time::timeout(Duration::from_secs(1), session) - .await - .expect("session shutdown") - .expect("session task") - .expect("session cleanup"); - assert_eq!( - nodes.lifecycle(node).await.expect("lifecycle"), - Lifecycle::Offline - ); -} - -fn message(sequence: u64, body: node_message::Body) -> NodeMessage { - NodeMessage { - sequence, - message_id: format!("message-{sequence}"), - idempotency_key: format!("message-{sequence}"), - body: Some(body), - } -} - -fn capabilities() -> Capabilities { - Capabilities { - architecture: "amd64".to_owned(), - operating_system: "linux".to_owned(), - container_runtime: "docker".to_owned(), - container_runtime_version: "29.6.2".to_owned(), - cores: 2, - memory_bytes: 8 * 1024 * 1024 * 1024, - labels: vec!["ubuntu-latest".to_owned()], - } -} - -fn capacity() -> Capacity { - Capacity { - build_volume_free_bytes: 60 * 1024 * 1024 * 1024, - layer_store_bytes: 10 * 1024 * 1024 * 1024, - cache_volume_present: true, - cache_volume_total_bytes: 100, - cache_volume_used_bytes: 40, - cache_volume_path: "/var/cache/syncode".to_owned(), - cached_images: Vec::new(), - cached_actions: Vec::new(), - } -} +#![allow(clippy::expect_used, clippy::panic)] + +use std::time::Duration; + +use syncode_control_nodes::{Ephemeral, Lifecycle, Nodes, Scope}; +use syncode_control_runs::{Forgotten, Runs}; +use tokio::sync::mpsc; +use tokio_stream::wrappers::ReceiverStream; + +use super::{NodeSessionServer, run_session}; +use crate::wire::{ + Capabilities, Capacity, Enrol, Heartbeat, Hello, NodeMessage, control_message, node_message, +}; +use crate::{ArtifactTokenAuthority, CapabilityAuthority, RepositoryCoordinates}; + +#[tokio::test] +async fn an_unreadable_client_releases_its_session() { + let runs = Runs::restored(Forgotten::default()).await.expect("runs"); + let nodes = Nodes::restored(Ephemeral::default()).await.expect("nodes"); + let token = nodes + .issue_token(Scope::Instance) + .await + .expect("enrolment token"); + let server = NodeSessionServer::new( + runs, + nodes.clone(), + CapabilityAuthority::new("test-capability-key").expect("capability authority"), + ArtifactTokenAuthority::new("test-artifact-key").expect("artifact authority"), + "http://127.0.0.1:1/".parse().expect("artifact URL"), + RepositoryCoordinates::new("http://127.0.0.1:1".to_owned(), String::new()) + .expect("repository coordinates"), + ); + let (input, input_receiver) = mpsc::channel(8); + let (output, mut output_receiver) = mpsc::channel(8); + let session = tokio::spawn(async move { + let mut inbound = ReceiverStream::new(input_receiver); + run_session(&mut inbound, &output, server).await + }); + + input + .send(Ok(message( + 1, + node_message::Body::Enrol(Enrol { + token: token.secret().expose().to_owned(), + }), + ))) + .await + .expect("enrol"); + let enrolled = output_receiver + .recv() + .await + .expect("enrolment response") + .expect("enrolment status"); + let control_message::Body::Enrolled(enrolled) = enrolled.body.expect("enrolment body") else { + panic!("expected enrolment"); + }; + let node = enrolled.node.parse().expect("node id"); + + input + .send(Ok(message( + 2, + node_message::Body::Hello(Hello { + node: enrolled.node, + credential: enrolled.credential, + capabilities: Some(capabilities()), + capacity: Some(capacity()), + max_parallel: 1, + }), + ))) + .await + .expect("hello"); + let welcome = output_receiver + .recv() + .await + .expect("welcome response") + .expect("welcome status"); + assert!(matches!( + welcome.body, + Some(control_message::Body::Welcome(_)) + )); + drop(output_receiver); + + input + .send(Ok(message( + 3, + node_message::Body::Heartbeat(Heartbeat { + held: Vec::new(), + capacity: Some(capacity()), + }), + ))) + .await + .expect("heartbeat"); + tokio::time::timeout(Duration::from_secs(1), session) + .await + .expect("session shutdown") + .expect("session task") + .expect("session cleanup"); + assert_eq!( + nodes.lifecycle(node).await.expect("lifecycle"), + Lifecycle::Offline + ); +} + +fn message(sequence: u64, body: node_message::Body) -> NodeMessage { + NodeMessage { + sequence, + message_id: format!("message-{sequence}"), + idempotency_key: format!("message-{sequence}"), + body: Some(body), + } +} + +fn capabilities() -> Capabilities { + Capabilities { + architecture: "amd64".to_owned(), + operating_system: "linux".to_owned(), + container_runtime: "docker".to_owned(), + container_runtime_version: "29.6.2".to_owned(), + cores: 2, + memory_bytes: 8 * 1024 * 1024 * 1024, + labels: vec!["ubuntu-latest".to_owned()], + } +} + +fn capacity() -> Capacity { + Capacity { + build_volume_free_bytes: 60 * 1024 * 1024 * 1024, + layer_store_bytes: 10 * 1024 * 1024 * 1024, + cache_volume_present: true, + cache_volume_total_bytes: 100, + cache_volume_used_bytes: 40, + cache_volume_path: "/var/cache/syncode".to_owned(), + cached_images: Vec::new(), + cached_actions: Vec::new(), + } +} diff --git a/tests/projection_client.rs b/tests/repository_coordinates.rs --- a/tests/projection_client.rs +++ b/tests/repository_coordinates.rs @@ -1,138 +1,90 @@ -#![allow(clippy::expect_used)] - -use std::error::Error; - -use axum::Json; -use axum::http::StatusCode; -use axum::routing::post; -use syncode_control_node::ProjectionClient; -use syncode_control_node::identity_wire::identity_server::{Identity, IdentityServer}; -use syncode_control_node::identity_wire::{ - CheckCapabilityRequest, CheckCapabilityResponse, GetRepositoryCoordinatesRequest, - GetRepositoryCoordinatesResponse, IssueWorkflowRepositoryTokenRequest, - IssueWorkflowRepositoryTokenResponse, ResolveRepositoryRequest, ResolveRepositoryResponse, - ValidateSessionRequest, ValidateSessionResponse, -}; -use syncode_control_runs::{Forgotten, JobId, Origin, Runs}; -use tokio::net::TcpListener; -use tokio::sync::mpsc; -use tokio_stream::wrappers::TcpListenerStream; -use tonic::transport::Server; -use tonic::{Request, Response, Status}; - -type TestResult = Result>; - -struct FixtureIdentity; - -#[tonic::async_trait] -impl Identity for FixtureIdentity { - async fn issue_workflow_repository_token( - &self, - _request: Request, - ) -> Result, Status> { - Err(Status::unimplemented("issue_workflow_repository_token")) - } - - async fn validate_session( - &self, - _request: Request, - ) -> Result, Status> { - Err(Status::unimplemented("validate_session")) - } - - async fn check_capability( - &self, - _request: Request, - ) -> Result, Status> { - Err(Status::unimplemented("check_capability")) - } - - async fn resolve_repository( - &self, - _request: Request, - ) -> Result, Status> { - Err(Status::unimplemented("resolve_repository")) - } - - async fn get_repository_coordinates( - &self, - request: Request, - ) -> Result, Status> { - if request - .metadata() - .get("authorization") - .and_then(|value| value.to_str().ok()) - != Some("Bearer shared-secret") - { - return Err(Status::unauthenticated("missing authorization")); - } - Ok(Response::new(GetRepositoryCoordinatesResponse { - owner: "syncode".to_owned(), - name: "pipelines-demo".to_owned(), - })) - } -} - -#[tokio::test] -async fn native_repository_runs_are_projected_with_legacy_coordinates() -> TestResult { - let identity_listener = TcpListener::bind("127.0.0.1:0").await?; - let identity_endpoint = format!("http://{}", identity_listener.local_addr()?); - tokio::spawn(async move { - let _ = Server::builder() - .add_service(IdentityServer::new(FixtureIdentity)) - .serve_with_incoming(TcpListenerStream::new(identity_listener)) - .await; - }); - - let projection_listener = TcpListener::bind("127.0.0.1:0").await?; - let projection_base = format!("http://{}/", projection_listener.local_addr()?).parse()?; - let (sent, mut received) = mpsc::unbounded_channel(); - let app = axum::Router::new().route( - "/api/internal/actions/syncode/projection", - post(move |Json(body): Json| { - let sent = sent.clone(); - async move { - let _ = sent.send(body); - StatusCode::NO_CONTENT - } - }), - ); - tokio::spawn(async move { - let _ = axum::serve(projection_listener, app).await; - }); - - let runs = Runs::restored(Forgotten::default()).await?; - let run = runs - .queue( - JobId::fresh(), - Origin::new( - "76128383-1df5-4979-9b13-c048a5287e9a".to_owned(), - "commit".to_owned(), - "refs/heads/main".to_owned(), - "push".to_owned(), - ".gitea/workflows/ci.yml".to_owned(), - ) - .with_delivery(Some("delivery".to_owned())), - b"plan".to_vec(), - ) - .await?; - let projection = runs.projection(run).await?.expect("projection"); - let client = ProjectionClient::new( - projection_base, - String::new(), - identity_endpoint, - "shared-secret".to_owned(), - )?; - assert_eq!( - client - .repository_coordinates("76128383-1df5-4979-9b13-c048a5287e9a") - .await?, - "syncode/pipelines-demo" - ); - client.send(&projection).await?; - - let body = received.recv().await.expect("projection body"); - assert_eq!(body["origin"]["repository"], "syncode/pipelines-demo"); - assert_eq!(body["origin"]["native"], true); - Ok(()) -} +#![allow(clippy::expect_used)] + +use std::error::Error; + +use syncode_control_node::RepositoryCoordinates; +use syncode_control_node::identity_wire::identity_server::{Identity, IdentityServer}; +use syncode_control_node::identity_wire::{ + CheckCapabilityRequest, CheckCapabilityResponse, GetRepositoryCoordinatesRequest, + GetRepositoryCoordinatesResponse, IssueWorkflowRepositoryTokenRequest, + IssueWorkflowRepositoryTokenResponse, ResolveRepositoryRequest, ResolveRepositoryResponse, + ValidateSessionRequest, ValidateSessionResponse, +}; +use tokio::net::TcpListener; +use tokio_stream::wrappers::TcpListenerStream; +use tonic::transport::Server; +use tonic::{Request, Response, Status}; + +type TestResult = Result>; + +struct FixtureIdentity; + +#[tonic::async_trait] +impl Identity for FixtureIdentity { + async fn issue_workflow_repository_token( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("issue_workflow_repository_token")) + } + + async fn validate_session( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("validate_session")) + } + + async fn check_capability( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("check_capability")) + } + + async fn resolve_repository( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("resolve_repository")) + } + + async fn get_repository_coordinates( + &self, + request: Request, + ) -> Result, Status> { + if request + .metadata() + .get("authorization") + .and_then(|value| value.to_str().ok()) + != Some("Bearer shared-secret") + { + return Err(Status::unauthenticated("missing authorization")); + } + Ok(Response::new(GetRepositoryCoordinatesResponse { + owner: "syncode".to_owned(), + name: "pipelines-demo".to_owned(), + })) + } +} + +#[tokio::test] +async fn native_repository_ids_resolve_to_coordinates() -> TestResult { + let identity_listener = TcpListener::bind("127.0.0.1:0").await?; + let identity_endpoint = format!("http://{}", identity_listener.local_addr()?); + tokio::spawn(async move { + let _ = Server::builder() + .add_service(IdentityServer::new(FixtureIdentity)) + .serve_with_incoming(TcpListenerStream::new(identity_listener)) + .await; + }); + + let client = RepositoryCoordinates::new(identity_endpoint, "shared-secret".to_owned())?; + assert_eq!( + client + .resolve("76128383-1df5-4979-9b13-c048a5287e9a") + .await?, + "syncode/pipelines-demo" + ); + Ok(()) +} diff --git a/crates/control-node/src/projection.rs b/crates/control-node/src/projection.rs --- a/crates/control-node/src/projection.rs +++ /dev/null @@ -1,184 +1,0 @@ -use serde::Serialize; -use syncode_control_runs::{ProjectedJob, ProjectedRun, RunId, RunNumber}; -use thiserror::Error; -use tonic::Request; -use tonic::metadata::{Ascii, MetadataValue}; -use tonic::transport::{Channel, Endpoint}; -use url::Url; -use uuid::Uuid; - -use crate::identity_wire::GetRepositoryCoordinatesRequest; -use crate::identity_wire::identity_client::IdentityClient; - -/// Pushes a run's projected state to the forge. Dispatch uses it to close the -/// gap between handing a node a token and the forge knowing what that token is -/// for; the periodic sweep in the binary uses it for everything after. -#[derive(Clone)] -pub struct ProjectionClient { - endpoint: Url, - token: String, - client: reqwest::Client, - identity: IdentityClient, - authorization: MetadataValue, -} - -#[derive(Debug, Error)] -pub enum ProjectionClientError { - #[error("cannot address the projection endpoint")] - Address, - #[error("cannot address the identity endpoint")] - IdentityAddress, - #[error("cannot authorize identity requests")] - IdentityAuthorization, -} - -#[derive(Debug, Error)] -pub enum ProjectionRequestError { - #[error(transparent)] - Transport(#[from] reqwest::Error), - #[error("HTTP {status}: {body}")] - Rejected { - status: reqwest::StatusCode, - body: String, - }, - #[error("identity rejected the repository projection: {0}")] - Identity(#[from] tonic::Status), - #[error("invalid repository projection origin {0:?}")] - InvalidRepository(String), -} - -#[derive(Serialize)] -struct LegacyProjection<'a> { - run: &'a RunId, - number: &'a RunNumber, - origin: LegacyOrigin<'a>, - sequence: u64, - jobs: &'a [ProjectedJob], -} - -#[derive(Serialize)] -struct LegacyOrigin<'a> { - repository: String, - commit: &'a str, - reference: &'a str, - event: &'a str, - workflow: &'a str, - delivery: Option<&'a str>, - principal: Option<&'a str>, - secrets_allowed: bool, - native: bool, -} - -impl ProjectionClient { - pub fn new( - base: Url, - token: String, - identity_endpoint: String, - identity_shared_secret: String, - ) -> Result { - let endpoint = base - .join("api/internal/actions/syncode/projection") - .map_err(|_| ProjectionClientError::Address)?; - let identity = IdentityClient::new( - Endpoint::from_shared(identity_endpoint) - .map_err(|_| ProjectionClientError::IdentityAddress)? - .connect_lazy(), - ); - let authorization = format!("Bearer {identity_shared_secret}") - .parse() - .map_err(|_| ProjectionClientError::IdentityAuthorization)?; - Ok(Self { - endpoint, - token, - client: reqwest::Client::new(), - identity, - authorization, - }) - } - - pub async fn send(&self, projection: &ProjectedRun) -> Result<(), ProjectionRequestError> { - let projection = self.for_legacy_projection(projection).await?; - let response = self - .client - .post(self.endpoint.clone()) - .header("X-Gitea-Internal-Auth", format!("Bearer {}", self.token)) - .json(&projection) - .send() - .await?; - let status = response.status(); - if status.is_success() { - return Ok(()); - } - let body = response.text().await?; - Err(ProjectionRequestError::Rejected { status, body }) - } - - async fn for_legacy_projection<'a>( - &self, - projection: &'a ProjectedRun, - ) -> Result, ProjectionRequestError> { - let repository = projection.origin.repository(); - let native = Uuid::parse_str(repository).is_ok(); - let repository = self.repository_coordinates(repository).await?; - Ok(LegacyProjection { - run: &projection.run, - number: &projection.number, - origin: LegacyOrigin { - repository, - commit: projection.origin.commit(), - reference: projection.origin.reference(), - event: projection.origin.event(), - workflow: projection.origin.workflow(), - delivery: projection.origin.delivery(), - principal: projection.origin.principal(), - secrets_allowed: projection.origin.secrets_allowed(), - native, - }, - sequence: projection.sequence, - jobs: &projection.jobs, - }) - } - - pub async fn repository_coordinates( - &self, - repository: &str, - ) -> Result { - if Uuid::parse_str(repository).is_ok() { - let mut request = Request::new(GetRepositoryCoordinatesRequest { - repository_id: repository.to_owned(), - }); - request - .metadata_mut() - .insert("authorization", self.authorization.clone()); - let coordinates = self - .identity - .clone() - .get_repository_coordinates(request) - .await? - .into_inner(); - if coordinates.owner.is_empty() - || coordinates.name.is_empty() - || coordinates.owner.contains('/') - || coordinates.name.contains('/') - { - return Err(ProjectionRequestError::InvalidRepository(format!( - "{}/{}", - coordinates.owner, coordinates.name - ))); - } - Ok(format!("{}/{}", coordinates.owner, coordinates.name)) - } else { - let Some((owner, name)) = repository.split_once('/') else { - return Err(ProjectionRequestError::InvalidRepository( - repository.to_owned(), - )); - }; - if owner.is_empty() || name.is_empty() || name.contains('/') { - return Err(ProjectionRequestError::InvalidRepository( - repository.to_owned(), - )); - } - Ok(repository.to_owned()) - } - } -} diff --git a/crates/control-node/src/repository_coordinates.rs b/crates/control-node/src/repository_coordinates.rs --- /dev/null +++ b/crates/control-node/src/repository_coordinates.rs @@ -1,0 +1,92 @@ +use thiserror::Error; +use tonic::Request; +use tonic::metadata::{Ascii, MetadataValue}; +use tonic::transport::{Channel, Endpoint}; +use uuid::Uuid; + +use crate::identity_wire::GetRepositoryCoordinatesRequest; +use crate::identity_wire::identity_client::IdentityClient; + +#[derive(Clone)] +pub struct RepositoryCoordinates { + identity: IdentityClient, + authorization: MetadataValue, +} + +#[derive(Debug, Error)] +pub enum RepositoryCoordinatesError { + #[error("cannot address the identity endpoint")] + IdentityAddress, + #[error("cannot authorize identity requests")] + IdentityAuthorization, +} + +#[derive(Debug, Error)] +pub enum RepositoryCoordinateRequestError { + #[error("identity rejected the repository coordinate request: {0}")] + Identity(#[from] tonic::Status), + #[error("invalid repository origin {0:?}")] + InvalidRepository(String), +} + +impl RepositoryCoordinates { + pub fn new( + identity_endpoint: String, + identity_shared_secret: String, + ) -> Result { + let identity = IdentityClient::new( + Endpoint::from_shared(identity_endpoint) + .map_err(|_| RepositoryCoordinatesError::IdentityAddress)? + .connect_lazy(), + ); + let authorization = format!("Bearer {identity_shared_secret}") + .parse() + .map_err(|_| RepositoryCoordinatesError::IdentityAuthorization)?; + Ok(Self { + identity, + authorization, + }) + } + + pub async fn resolve( + &self, + repository: &str, + ) -> Result { + if Uuid::parse_str(repository).is_ok() { + let mut request = Request::new(GetRepositoryCoordinatesRequest { + repository_id: repository.to_owned(), + }); + request + .metadata_mut() + .insert("authorization", self.authorization.clone()); + let coordinates = self + .identity + .clone() + .get_repository_coordinates(request) + .await? + .into_inner(); + if coordinates.owner.is_empty() + || coordinates.name.is_empty() + || coordinates.owner.contains('/') + || coordinates.name.contains('/') + { + return Err(RepositoryCoordinateRequestError::InvalidRepository( + format!("{}/{}", coordinates.owner, coordinates.name), + )); + } + Ok(format!("{}/{}", coordinates.owner, coordinates.name)) + } else { + let Some((owner, name)) = repository.split_once('/') else { + return Err(RepositoryCoordinateRequestError::InvalidRepository( + repository.to_owned(), + )); + }; + if owner.is_empty() || name.is_empty() || name.contains('/') { + return Err(RepositoryCoordinateRequestError::InvalidRepository( + repository.to_owned(), + )); + } + Ok(repository.to_owned()) + } + } +} diff --git a/src/projection.rs b/src/projection.rs --- a/src/projection.rs +++ /dev/null @@ -1,53 +1,0 @@ -use std::collections::HashMap; -use std::time::Duration; - -use syncode_control_node::ProjectionClient; -use syncode_control_runs::{ProjectedRun, RunId, RunLog, Runs, RunsError}; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum ProjectionError { - #[error("cannot read the run projection: {0}")] - Runs(#[from] RunsError), - #[error("pending projection for run {0} is missing")] - MissingPending(RunId), -} - -pub async fn serve( - runs: Runs, - client: ProjectionClient, - min_run_number: u64, -) -> Result<(), ProjectionError> { - let mut delivered = HashMap::::new(); - let mut pending = HashMap::::new(); - loop { - for (run, number, sequence, output_version) in runs.projection_versions().await { - if number.get() < min_run_number { - continue; - } - let version = (sequence, output_version); - if delivered.get(&run) == Some(&version) { - continue; - } - if pending.get(&run).map(|(version, _)| *version) != Some(version) { - let Some(projection) = runs.projection(run).await? else { - continue; - }; - pending.insert(run, (version, projection)); - } - let (_, projection) = pending - .get(&run) - .ok_or(ProjectionError::MissingPending(run))?; - match client.send(projection).await { - Ok(()) => { - delivered.insert(run, version); - pending.remove(&run); - } - Err(error) => { - eprintln!("cannot project run state: {error}"); - } - } - } - tokio::time::sleep(Duration::from_millis(250)).await; - } -} -- SynCode