From b74ea44d19d971a5390233aa71a928ae739a8e71 Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Thu, 8 Jan 2026 15:40:48 +0100 Subject: [PATCH 01/21] impl first iter --- Cargo.lock | 674 +++++++++++++++++++++- Cargo.toml | 4 + crates/tower-runtime/Cargo.toml | 17 +- crates/tower-runtime/src/errors.rs | 9 + crates/tower-runtime/src/execution.rs | 781 ++++++++++++++++++++++++++ crates/tower-runtime/src/k8s.rs | 441 +++++++++++++++ crates/tower-runtime/src/lib.rs | 4 + crates/tower-runtime/src/local.rs | 176 ++++++ 8 files changed, 2089 insertions(+), 17 deletions(-) create mode 100644 crates/tower-runtime/src/execution.rs create mode 100644 crates/tower-runtime/src/k8s.rs diff --git a/Cargo.lock b/Cargo.lock index abe512d3..ce159829 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,19 @@ dependencies = [ "subtle", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.3", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -61,6 +74,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -132,6 +151,18 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-compression" version = "0.4.27" @@ -168,6 +199,17 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "async_zip" version = "0.0.16" @@ -243,6 +285,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "getrandom 0.2.16", + "instant", + "rand 0.8.5", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -258,6 +311,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -364,7 +423,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -478,6 +537,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "config" version = "0.3.39" @@ -517,6 +585,16 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[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" @@ -590,7 +668,7 @@ name = "crypto" version = "0.3.39" dependencies = [ "aes-gcm", - "base64", + "base64 0.22.1", "pem", "rand 0.8.5", "rsa", @@ -800,6 +878,17 @@ dependencies = [ "serde", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -937,6 +1026,27 @@ dependencies = [ "str-buf", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "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 = "eventsource-stream" version = "0.2.3" @@ -987,6 +1097,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fluent-uri" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1216,12 +1335,46 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + [[package]] name = "hashbrown" version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64 0.22.1", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.4.1" @@ -1240,6 +1393,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "http" version = "1.3.1" @@ -1312,6 +1474,26 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-http-proxy" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ad4b0a1e37510028bc4ba81d0e38d239c39671b0f0ce9e02dfa93a8133f7c08" +dependencies = [ + "bytes", + "futures-util", + "headers", + "http", + "hyper", + "hyper-rustls", + "hyper-util", + "pin-project-lite", + "rustls-native-certs 0.7.3", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-rustls" version = "0.27.7" @@ -1321,7 +1503,9 @@ dependencies = [ "http", "hyper", "hyper-util", + "log", "rustls", + "rustls-native-certs 0.8.3", "rustls-pki-types", "tokio", "tokio-rustls", @@ -1329,13 +1513,26 @@ dependencies = [ "webpki-roots", ] +[[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.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1556,6 +1753,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "inventory" version = "0.3.21" @@ -1645,6 +1851,167 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json-patch" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b1fb8864823fad91877e6caea0baca82e49e8db50f8e5c9f9a453e27d3330fc" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonpath-rust" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d8fe85bd70ff715f31ce8c739194b423d79811a19602115d611a3ec85d6200" +dependencies = [ + "lazy_static", + "once_cell", + "pest", + "pest_derive", + "regex", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c6e529149475ca0b2820835d3dce8fcc41c6b943ca608d32f35b449255e4627" +dependencies = [ + "fluent-uri", + "serde", + "serde_json", +] + +[[package]] +name = "k8s-openapi" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8847402328d8301354c94d605481f25a6bdc1ed65471fd96af8eca71141b13" +dependencies = [ + "base64 0.22.1", + "chrono", + "serde", + "serde-value", + "serde_json", +] + +[[package]] +name = "kube" +version = "0.95.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa21063c854820a77c5d7f8deeb7ffa55246d8304e4bcd8cce2956752c6604f8" +dependencies = [ + "k8s-openapi", + "kube-client", + "kube-core", + "kube-derive", + "kube-runtime", +] + +[[package]] +name = "kube-client" +version = "0.95.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c2355f5c9d8a11900e71a6fe1e47abd5ec45bf971eb4b162ffe97b46db9bb7" +dependencies = [ + "base64 0.22.1", + "bytes", + "chrono", + "either", + "futures", + "home", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-http-proxy", + "hyper-rustls", + "hyper-timeout", + "hyper-util", + "jsonpath-rust", + "k8s-openapi", + "kube-core", + "pem", + "rustls", + "rustls-pemfile", + "secrecy", + "serde", + "serde_json", + "serde_yaml", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "tower 0.4.13", + "tower-http 0.5.2", + "tracing", +] + +[[package]] +name = "kube-core" +version = "0.95.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3030bd91c9db544a50247e7d48d7db9cf633c172732dce13351854526b1e666" +dependencies = [ + "chrono", + "form_urlencoded", + "http", + "json-patch", + "k8s-openapi", + "schemars 0.8.22", + "serde", + "serde-value", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "kube-derive" +version = "0.95.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa98be978eddd70a773aa8e86346075365bfb7eb48783410852dbf7cb57f0c27" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "serde_json", + "syn 2.0.104", +] + +[[package]] +name = "kube-runtime" +version = "0.95.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5895cb8aa641ac922408f128b935652b34c2995f16ad7db0984f6caa50217914" +dependencies = [ + "ahash", + "async-broadcast", + "async-stream", + "async-trait", + "backoff", + "derivative", + "futures", + "hashbrown 0.14.5", + "json-patch", + "jsonptr", + "k8s-openapi", + "kube-client", + "parking_lot", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "lazy-regex" version = "3.4.1" @@ -2006,12 +2373,33 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-probe" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" + [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "overload" version = "0.1.1" @@ -2086,7 +2474,7 @@ version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" dependencies = [ - "base64", + "base64 0.22.1", "serde", ] @@ -2105,6 +2493,49 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "pest_meta" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +dependencies = [ + "pest", + "sha2", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -2462,7 +2893,7 @@ version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-core", "futures-util", @@ -2488,7 +2919,7 @@ dependencies = [ "tokio-rustls", "tokio-util", "tower 0.5.2", - "tower-http", + "tower-http 0.6.6", "tower-service", "url", "wasm-bindgen", @@ -2535,7 +2966,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2faf35b7d3c4b7f8c21c45bb014011b32a0ce6444bf6094da04daab01a8c3c34" dependencies = [ "axum", - "base64", + "base64 0.22.1", "bytes", "chrono", "futures", @@ -2657,6 +3088,7 @@ version = "0.23.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", @@ -2665,6 +3097,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe 0.1.6", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework 2.11.1", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe 0.2.0", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.12.0" @@ -2731,6 +3197,27 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive 0.8.22", + "serde", + "serde_json", +] + [[package]] name = "schemars" version = "0.9.0" @@ -2752,11 +3239,23 @@ dependencies = [ "chrono", "dyn-clone", "ref-cast", - "schemars_derive", + "schemars_derive 1.0.4", "serde", "serde_json", ] +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.104", +] + [[package]] name = "schemars_derive" version = "1.0.4" @@ -2787,6 +3286,52 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "serde", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.219" @@ -2796,6 +3341,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.219" @@ -2878,7 +3433,7 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" dependencies = [ - "base64", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", @@ -2904,6 +3459,30 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.10.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3439,6 +4018,7 @@ dependencies = [ "futures-io", "futures-sink", "pin-project-lite", + "slab", "tokio", ] @@ -3492,6 +4072,23 @@ dependencies = [ "tower-cmd", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.5.2" @@ -3566,6 +4163,25 @@ dependencies = [ "webbrowser", ] +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "base64 0.21.7", + "bitflags 2.9.1", + "bytes", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-http" version = "0.6.6" @@ -3613,8 +4229,11 @@ dependencies = [ name = "tower-runtime" version = "0.3.39" dependencies = [ + "async-trait", "chrono", "config", + "k8s-openapi", + "kube", "nix 0.30.1", "snafu", "tokio", @@ -3622,6 +4241,7 @@ dependencies = [ "tower-package", "tower-telemetry", "tower-uv", + "uuid", ] [[package]] @@ -3784,6 +4404,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicase" version = "2.8.1" @@ -3830,6 +4456,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" @@ -3868,6 +4500,7 @@ checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" dependencies = [ "getrandom 0.3.3", "js-sys", + "serde", "wasm-bindgen", ] @@ -4027,7 +4660,7 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aaf4f3c0ba838e82b4e5ccc4157003fb8c324ee24c058470ffb82820becbde98" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", "jni", "log", "ndk-context", @@ -4085,7 +4718,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings", ] @@ -4118,13 +4751,19 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -4133,7 +4772,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -4181,6 +4820,15 @@ dependencies = [ "windows-targets 0.53.2", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.42.2" diff --git a/Cargo.toml b/Cargo.toml index 3d4dbb79..570803c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ repository = "https://github.com/tower/tower-cli" aes-gcm = "0.10" anyhow = "1.0.95" async-compression = { version = "0.4", features = ["tokio", "gzip"] } +async-trait = "0.1.83" async_zip = { version = "0.0.16", features = ["tokio", "tokio-fs", "deflate"] } axum = "0.8.4" base64 = "0.22" @@ -33,6 +34,8 @@ futures-lite = "2.6" glob = "0.3" http = "1.1" indicatif = "0.17" +k8s-openapi = { version = "0.23", features = ["v1_31"] } +kube = { version = "0.95", features = ["runtime", "client", "derive"] } nix = { version = "0.30", features = ["signal"] } pem = "3" promptly = "0.3" @@ -65,6 +68,7 @@ tracing = { version = "0.1" } tracing-appender = "0.2" tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] } url = { version = "2", features = ["serde"] } +uuid = { version = "1.11", features = ["v4", "serde"] } webbrowser = "1" # The profile that 'dist' will build with diff --git a/crates/tower-runtime/Cargo.toml b/crates/tower-runtime/Cargo.toml index 8ab42fb5..237ae18f 100644 --- a/crates/tower-runtime/Cargo.toml +++ b/crates/tower-runtime/Cargo.toml @@ -7,14 +7,23 @@ rust-version = { workspace = true } license = { workspace = true } [dependencies] +async-trait = { workspace = true } chrono = { workspace = true } nix = { workspace = true } -snafu = { workspace = true } +snafu = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true } -tower-package = { workspace = true } -tower-telemetry = { workspace = true } -tower-uv = { workspace = true } +tower-package = { workspace = true } +tower-telemetry = { workspace = true } +tower-uv = { workspace = true } +uuid = { workspace = true } + +# Optional dependencies for K8s backend +kube = { workspace = true, optional = true } +k8s-openapi = { workspace = true, optional = true } + +[features] +k8s = ["kube", "k8s-openapi"] [dev-dependencies] config = { workspace = true } diff --git a/crates/tower-runtime/src/errors.rs b/crates/tower-runtime/src/errors.rs index 19c8ed10..8af364b8 100644 --- a/crates/tower-runtime/src/errors.rs +++ b/crates/tower-runtime/src/errors.rs @@ -64,6 +64,15 @@ pub enum Error { #[snafu(display("cancelled"))] Cancelled, + + #[snafu(display("app not started"))] + AppNotStarted, + + #[snafu(display("no execution handle"))] + NoHandle, + + #[snafu(display("invalid package"))] + InvalidPackage, } impl From for Error { diff --git a/crates/tower-runtime/src/execution.rs b/crates/tower-runtime/src/execution.rs new file mode 100644 index 00000000..f7e02ee4 --- /dev/null +++ b/crates/tower-runtime/src/execution.rs @@ -0,0 +1,781 @@ +//! Generic execution backend abstraction for Tower +//! +//! This module provides traits and types for abstracting execution backends, +//! allowing Tower to support multiple compute substrates (local processes, +//! Kubernetes pods, microVMs, gVisor, etc.) through a uniform interface. +//! +//! # Key Design Principles +//! +//! - **No language/runtime assumptions** - Python, Node.js, etc. are backend details +//! - **Cache is container filesystem-based** - Image layers, bundle mounts, not language-specific +//! - **Backends handle runtime setup** - Dependency installation, environment prep +//! - **Lifecycle operations are backend-agnostic** - Start/stop/status work everywhere +//! +//! # Security Model for Shared Caches +//! +//! Tower uses a **tiered cache isolation strategy** to balance performance and security: +//! +//! ## Safe for Global Sharing (Read-Only) +//! +//! These caches are **content-addressable** and **cryptographically verified**, making them +//! safe to share across all tenants: +//! +//! - **Bundle cache**: Keyed by SHA256 checksum, always read-only +//! - **Container layer cache**: Keyed by digest, always read-only +//! +//! **Attack surface**: Minimal - content is verified before use, mounted read-only +//! +//! ## Require Isolation (Read-Write) +//! +//! These caches are **mutable** and **writable**, requiring isolation: +//! +//! - **Dependency caches** (pip, npm, cargo, etc.): Writable by apps during installation +//! +//! **Attack vectors if shared globally**: +//! - **Cache poisoning**: Malicious app writes bad dependencies to shared cache +//! - **Information disclosure**: App reads another tenant's private packages +//! - **Timing attacks**: Infer what dependencies other tenants use +//! +//! **Mitigation**: Use `CacheIsolation::PerAccount` or `CacheIsolation::PerApp` +//! +//! ## Recommended Configuration +//! +//! ```rust,ignore +//! // For Tower's multi-tenant SaaS: +//! CacheConfig { +//! enable_bundle_cache: true, +//! enable_runtime_cache: true, +//! enable_dependency_cache: true, +//! isolation: CacheIsolation::PerAccount { account_id: "acct-123" }, +//! } +//! ``` +//! +//! This gives: +//! - ✅ Bundle/layer sharing across all tenants (fast) +//! - ✅ Dependency cache sharing within an account (reasonable hit rate) +//! - ✅ Isolation between different accounts (secure) + +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::mpsc::UnboundedReceiver; + +use crate::errors::Error; + +// ============================================================================ +// Core Execution Types +// ============================================================================ + +/// ExecutionSpec describes what to execute and how +#[derive(Debug, Clone)] +pub struct ExecutionSpec { + /// Unique identifier for this execution (e.g., run_id) + pub id: String, + + /// Bundle reference (how to get the application code) + pub bundle: BundleRef, + + /// Runtime configuration (image, version, etc.) + pub runtime: RuntimeConfig, + + /// Environment name (e.g., "production", "staging", "default") + pub environment: String, + + /// Secret key-value pairs to inject + pub secrets: HashMap, + + /// Parameter key-value pairs to inject + pub parameters: HashMap, + + /// Additional environment variables + pub env_vars: HashMap, + + /// Resource limits for execution + pub resources: ResourceLimits, + + /// Networking configuration (for service workloads) + pub networking: Option, + + /// Telemetry context for tracing + pub telemetry_ctx: tower_telemetry::Context, +} + +/// BundleRef describes where to get the application bundle +#[derive(Debug, Clone)] +pub enum BundleRef { + /// Local filesystem path (for local execution) + Local { path: PathBuf }, + + /// Remote bundle to be downloaded + Remote { + bundle_id: String, + checksum: String, + download_url: String, + }, + + /// Container image reference + ContainerImage { + registry: String, + repository: String, + tag: String, + digest: Option, + }, +} + +/// RuntimeConfig specifies the execution runtime environment +#[derive(Debug, Clone)] +pub struct RuntimeConfig { + /// Runtime image to use (e.g., "towerhq/tower-runtime:python-3.11") + pub image: String, + + /// Specific version/tag if applicable + pub version: Option, + + /// Cache configuration + pub cache: CacheConfig, + + /// Entrypoint override (if not using bundle's default) + pub entrypoint: Option>, + + /// Command override (if not using bundle's default) + pub command: Option>, +} + +/// CacheConfig describes what should be cached +#[derive(Debug, Clone)] +pub struct CacheConfig { + /// Enable bundle caching (content-addressable by checksum) + pub enable_bundle_cache: bool, + + /// Enable runtime layer caching (container image layers) + pub enable_runtime_cache: bool, + + /// Enable dependency caching (language-specific, e.g., pip cache, node_modules) + pub enable_dependency_cache: bool, + + /// Cache backend to use + pub backend: CacheBackend, + + /// Cache isolation strategy + pub isolation: CacheIsolation, +} + +/// CacheIsolation defines security boundaries for caches +#[derive(Debug, Clone)] +pub enum CacheIsolation { + /// Global sharing (safe for content-addressable immutable content only) + /// + /// Use for: bundles (by checksum), container layers (by digest) + /// Security: Read-only mounts, content is cryptographically verified + Global, + + /// Per-account isolation (share within account, isolate between accounts) + /// + /// Use for: dependency caches when you trust apps within same account + /// Security: Apps in account A cannot access account B's cache + PerAccount { account_id: String }, + + /// Per-app isolation (completely isolated, no sharing) + /// + /// Use for: maximum security, mutable caches in multi-tenant environments + /// Security: Each app has its own cache namespace + PerApp { app_id: String }, + + /// No isolation (single-tenant environments only) + /// + /// WARNING: Only use in trusted single-tenant environments + None, +} + +/// CacheBackend describes where caches are stored +#[derive(Debug, Clone)] +pub enum CacheBackend { + /// Local filesystem cache + Local { cache_dir: PathBuf }, + + /// Kubernetes PersistentVolume + K8sPersistentVolume { pv_claim_name: String }, + + /// S3-based cache + S3 { bucket: String, prefix: String }, + + /// No caching + None, +} + +/// ResourceLimits defines compute resource constraints +#[derive(Debug, Clone)] +pub struct ResourceLimits { + /// CPU limit in millicores (e.g., 1000 = 1 CPU) + pub cpu_millicores: Option, + + /// Memory limit in megabytes + pub memory_mb: Option, + + /// Ephemeral storage limit in megabytes + pub storage_mb: Option, + + /// Maximum number of processes (prevents fork bombs) + pub max_pids: Option, + + /// GPU count (0 = no GPU) + pub gpu_count: u32, + + /// Execution timeout in seconds (0 = no timeout) + /// Control plane sets policy, runner may apply local ceiling + pub timeout_seconds: u32, +} + +/// NetworkingSpec defines networking requirements +#[derive(Debug, Clone)] +pub struct NetworkingSpec { + /// Port the app listens on + pub port: u16, + + /// Whether this app needs a stable service endpoint + pub expose_service: bool, + + /// Service name (for DNS) + pub service_name: Option, +} + +// ============================================================================ +// Execution Backend Trait +// ============================================================================ + +/// ExecutionBackend abstracts the compute substrate (K8s, microVM, gVisor, etc.) +#[async_trait] +pub trait ExecutionBackend: Send + Sync { + /// The handle type this backend returns + type Handle: ExecutionHandle; + + /// Create a new execution environment + /// + /// This method is responsible for: + /// - Acquiring/downloading the bundle + /// - Setting up the runtime environment (using cache if available) + /// - Starting the application + /// - Returning a handle for lifecycle management + async fn create(&self, spec: ExecutionSpec) -> Result; + + /// Get backend capabilities (for optimization hints) + fn capabilities(&self) -> BackendCapabilities; + + /// Cleanup backend resources (called on shutdown) + async fn cleanup(&self) -> Result<(), Error>; +} + +/// BackendCapabilities describes what a backend supports +#[derive(Debug, Clone)] +pub struct BackendCapabilities { + /// Backend name (for debugging/logging) + pub name: String, + + /// Does backend support persistent volumes for caching? + pub supports_persistent_cache: bool, + + /// Does backend support pre-warmed environments? + pub supports_prewarming: bool, + + /// Does backend support network isolation? + pub supports_network_isolation: bool, + + /// Does backend support service endpoints? + pub supports_service_endpoints: bool, + + /// Typical startup latency characteristics (milliseconds) + pub typical_cold_start_ms: u64, + pub typical_warm_start_ms: u64, + + /// Maximum concurrent executions this backend can handle + pub max_concurrent_executions: Option, +} + +// ============================================================================ +// Execution Handle Trait +// ============================================================================ + +/// ExecutionHandle represents a running execution (Pod, microVM, process, etc.) +#[async_trait] +pub trait ExecutionHandle: Send + Sync { + /// Get unique identifier for this execution + fn id(&self) -> &str; + + /// Get current execution status + async fn status(&self) -> Result; + + /// Subscribe to log stream (returns receiver for log lines) + /// + /// Note: Multiple calls may return the same stream or separate streams + /// depending on backend implementation + async fn logs(&self) -> Result; + + /// Terminate execution gracefully (SIGTERM equivalent) + /// + /// Returns when termination is initiated, not necessarily complete. + /// Use `wait_for_completion()` to wait for actual termination. + async fn terminate(&mut self) -> Result<(), Error>; + + /// Force kill execution (SIGKILL equivalent) + async fn kill(&mut self) -> Result<(), Error>; + + /// Wait for execution to complete (blocking) + async fn wait_for_completion(&self) -> Result; + + /// Get service endpoint (if this execution exposes a service) + async fn service_endpoint(&self) -> Result, Error>; + + /// Cleanup resources (delete pod, cleanup filesystem, etc.) + /// + /// Should be called after execution completes to free resources. + async fn cleanup(&mut self) -> Result<(), Error>; +} + +/// ExecutionStatus represents the current state of an execution +#[derive(Debug, Clone, PartialEq)] +pub enum ExecutionStatus { + /// Execution is being prepared (downloading bundle, setting up environment) + Preparing, + + /// Execution is currently running + Running, + + /// Execution completed successfully (exit code 0) + Succeeded, + + /// Execution failed with non-zero exit code + Failed { exit_code: i32 }, + + /// Execution crashed (segfault, OOM kill, etc.) + Crashed { reason: String }, + + /// Execution was terminated by user/system + Terminated, + + /// Unknown status (shouldn't happen in normal operation) + Unknown, +} + +/// ServiceEndpoint describes how to reach a running service +#[derive(Debug, Clone)] +pub struct ServiceEndpoint { + /// Host/IP to connect to + pub host: String, + + /// Port to connect to + pub port: u16, + + /// Protocol (http, https, tcp, etc.) + pub protocol: String, + + /// Full URL if applicable (e.g., "http://app-run-123.default.svc.cluster.local:8080") + pub url: Option, +} + +// ============================================================================ +// Log Streaming Types +// ============================================================================ + +/// LogReceiver is a stream of log lines from the execution +pub type LogReceiver = UnboundedReceiver; + +/// LogLine represents a single line of output +#[derive(Debug, Clone)] +pub struct LogLine { + /// When this line was emitted + pub timestamp: DateTime, + + /// Which stream (stdout/stderr) + pub stream: LogStream, + + /// Which phase (setup vs program) + pub channel: LogChannel, + + /// The actual log content + pub content: String, +} + +/// LogStream identifies stdout vs stderr +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LogStream { + Stdout, + Stderr, +} + +/// LogChannel identifies setup vs program output +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LogChannel { + /// Setup phase (dependency installation, environment prep) + Setup, + + /// Program phase (actual application output) + Program, +} + +// ============================================================================ +// App Trait Integration (High-Level Interface) +// ============================================================================ + +/// App trait provides high-level lifecycle management +/// +/// This trait is the user-facing interface used by AppLauncher and CLI code. +/// Implementations use ExecutionBackend internally to provide isolation. +#[async_trait] +pub trait App: Send + Sync { + /// The backend type this App uses for execution + type Backend: ExecutionBackend; + + /// Start a new execution + /// + /// The backend is passed explicitly to avoid static method limitations. + async fn start(backend: Arc, opts: StartOptions) -> Result + where + Self: Sized; + + /// Get current execution status + async fn status(&self) -> Result; + + /// Terminate execution gracefully + async fn terminate(&mut self) -> Result<(), Error>; + + /// Get service endpoint (if applicable) + async fn service_endpoint(&self) -> Result, Error> { + Ok(None) + } +} + +/// StartOptions contains all parameters needed to start an execution +/// +/// This is the same structure used by existing CLI/runner code, kept for compatibility. +pub struct StartOptions { + pub ctx: tower_telemetry::Context, + pub package: tower_package::Package, + pub cwd: Option, + pub environment: String, + pub secrets: HashMap, + pub parameters: HashMap, + pub env_vars: HashMap, + pub output_sender: tokio::sync::mpsc::UnboundedSender, + pub cache_dir: Option, +} + +/// AppLauncher orchestrates App lifecycle +/// +/// Generic over App type, which determines the backend via associated type. +pub struct AppLauncher { + backend: Arc, + app: Option, +} + +impl AppLauncher { + /// Create a new launcher with the specified backend + pub fn new(backend: Arc) -> Self { + Self { backend, app: None } + } + + /// Launch an app with the given options + pub async fn launch(&mut self, opts: StartOptions) -> Result<(), Error> { + // Drop any existing app + self.app = None; + + // Start new app using backend + let app = A::start(self.backend.clone(), opts).await?; + self.app = Some(app); + + Ok(()) + } + + /// Get current app status + pub async fn status(&self) -> Result { + self.app + .as_ref() + .ok_or(Error::AppNotStarted)? + .status() + .await + } + + /// Terminate the running app + pub async fn terminate(&mut self) -> Result<(), Error> { + if let Some(app) = &mut self.app { + app.terminate().await?; + self.app = None; + } + Ok(()) + } + + /// Get service endpoint (if app exposes one) + pub async fn service_endpoint(&self) -> Result, Error> { + match &self.app { + Some(app) => app.service_endpoint().await, + None => Ok(None), + } + } +} + +/// ManagedApp is the primary implementation of App that uses ExecutionBackend +/// +/// This is the bridge between the high-level App trait and low-level ExecutionBackend. +pub struct ManagedApp { + backend: Arc, + handle: Option, + output_forwarder: Option>, +} + +#[async_trait] +impl App for ManagedApp { + type Backend = B; + + async fn start(backend: Arc, opts: StartOptions) -> Result { + // Extract output_sender before consuming opts + let output_sender = opts.output_sender.clone(); + + // Convert StartOptions to ExecutionSpec (consumes opts) + let spec = convert_start_options_to_spec(opts)?; + + // Create execution using backend + let handle = backend.create(spec).await?; + + // Start log forwarding + let mut log_receiver = handle.logs().await?; + let output_forwarder = tokio::spawn(async move { + while let Some(log_line) = log_receiver.recv().await { + // Convert LogLine to Output for existing code + let _ = output_sender.send(crate::Output { + channel: match log_line.channel { + LogChannel::Setup => crate::Channel::Setup, + LogChannel::Program => crate::Channel::Program, + }, + time: log_line.timestamp, + fd: match log_line.stream { + LogStream::Stdout => crate::FD::Stdout, + LogStream::Stderr => crate::FD::Stderr, + }, + line: log_line.content, + }); + } + }); + + Ok(Self { + backend, + handle: Some(handle), + output_forwarder: Some(output_forwarder), + }) + } + + async fn status(&self) -> Result { + self.handle.as_ref().ok_or(Error::NoHandle)?.status().await + } + + async fn terminate(&mut self) -> Result<(), Error> { + if let Some(mut handle) = self.handle.take() { + handle.terminate().await?; + handle.cleanup().await?; + } + + // Stop log forwarding + if let Some(forwarder) = self.output_forwarder.take() { + forwarder.abort(); + } + + Ok(()) + } + + async fn service_endpoint(&self) -> Result, Error> { + match &self.handle { + Some(handle) => handle.service_endpoint().await, + None => Ok(None), + } + } +} + +/// Helper function to convert StartOptions to ExecutionSpec +fn convert_start_options_to_spec(opts: StartOptions) -> Result { + let package = opts.package; + + // Determine bundle reference from package + let bundle = if let Some(path) = package.unpacked_path { + BundleRef::Local { path } + } else { + // TODO: Handle remote bundles + return Err(Error::InvalidPackage); + }; + + // Build runtime config from cache_dir + let runtime = RuntimeConfig { + image: "towerhq/tower-runtime:latest".to_string(), // TODO: Make configurable + version: None, + cache: CacheConfig { + enable_bundle_cache: true, + enable_runtime_cache: true, + enable_dependency_cache: true, + backend: match opts.cache_dir { + Some(dir) => CacheBackend::Local { cache_dir: dir }, + None => CacheBackend::None, + }, + isolation: CacheIsolation::None, // TODO: Get from context + }, + entrypoint: None, + command: None, + }; + + Ok(ExecutionSpec { + id: uuid::Uuid::new_v4().to_string(), // TODO: Use actual run_id + bundle, + runtime, + environment: opts.environment, + secrets: opts.secrets, + parameters: opts.parameters, + env_vars: opts.env_vars, + resources: ResourceLimits { + cpu_millicores: None, + memory_mb: None, + storage_mb: None, + max_pids: None, + gpu_count: 0, + timeout_seconds: 0, // No timeout by default + }, + networking: None, + telemetry_ctx: opts.ctx, + }) +} + +// ============================================================================ +// Cache Manager Trait +// ============================================================================ + +/// CacheManager abstracts caching across execution backends +#[async_trait] +pub trait CacheManager: Send + Sync { + /// Get cached bundle, return handle if hit + /// + /// Bundles are content-addressable by checksum, safe for global sharing. + /// Always returns read-only handle. + async fn get_bundle( + &self, + bundle_id: &str, + checksum: &str, + ) -> Result, Error>; + + /// Store bundle in cache + /// + /// Bundles are stored globally with CacheIsolation::Global + async fn put_bundle( + &self, + bundle_id: &str, + checksum: &str, + source: CacheSource, + ) -> Result; + + /// Get cached runtime layer, return handle if hit + /// + /// Runtime layers are content-addressable by digest, safe for global sharing. + /// Always returns read-only handle. + async fn get_runtime_layer( + &self, + image: &str, + layer_digest: &str, + ) -> Result, Error>; + + /// Store runtime layer in cache + /// + /// Layers are stored globally with CacheIsolation::Global + async fn put_runtime_layer( + &self, + image: &str, + layer_digest: &str, + source: CacheSource, + ) -> Result; + + /// Get cache directory for language-specific dependencies + /// + /// Returns a handle to a cache directory that can be mounted into + /// the execution environment (e.g., for pip cache, npm cache, etc.) + /// + /// SECURITY: This returns a read-write handle isolated by the provided isolation strategy. + /// - CacheIsolation::Global: NOT RECOMMENDED (writable by all apps) + /// - CacheIsolation::PerAccount: Shared within account (recommended for Tower) + /// - CacheIsolation::PerApp: Fully isolated per app (maximum security) + async fn get_dependency_cache( + &self, + language: &str, + isolation: CacheIsolation, + ) -> Result; + + /// Cleanup old cache entries (LRU eviction, TTL expiration, etc.) + async fn cleanup(&self, max_age_seconds: u64) -> Result; +} + +/// CacheHandle represents a cached item that can be used by a backend +#[derive(Debug, Clone)] +pub struct CacheHandle { + /// The actual storage location + pub location: CacheLocation, + + /// Access permissions for this cache + pub permissions: CachePermissions, + + /// Isolation context (which account/app can access this) + pub isolation: CacheIsolation, +} + +/// CacheLocation describes where the cached data lives +#[derive(Debug, Clone)] +pub enum CacheLocation { + /// Local filesystem path + Local(PathBuf), + + /// Kubernetes PersistentVolume mount specification + K8sVolume { + pv_claim_name: String, + subpath: String, + }, + + /// S3 location + S3 { + bucket: String, + key: String, + etag: Option, + }, + + /// Container image layer (already in container runtime) + ContainerLayer { layer_id: String }, +} + +/// CachePermissions defines what operations are allowed on cached data +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CachePermissions { + /// Read-only access (for immutable content like bundles, layers) + ReadOnly, + + /// Read-write access (for mutable caches like dependency caches) + ReadWrite, +} + +/// CacheSource describes where to get data for caching +#[derive(Debug)] +pub enum CacheSource { + /// Copy from local filesystem + LocalPath(PathBuf), + + /// Download from URL + Url(String), + + /// Raw bytes + Bytes(Vec), +} + +/// CleanupStats reports cache cleanup results +#[derive(Debug, Clone)] +pub struct CleanupStats { + pub entries_removed: u32, + pub bytes_freed: u64, +} + +// ============================================================================ +// Concrete Backend Implementations +// ============================================================================ + +// LocalBackend and K8sBackend will be implemented in separate modules +// See local.rs and k8s.rs (feature-gated) diff --git a/crates/tower-runtime/src/k8s.rs b/crates/tower-runtime/src/k8s.rs new file mode 100644 index 00000000..356797ef --- /dev/null +++ b/crates/tower-runtime/src/k8s.rs @@ -0,0 +1,441 @@ +//! Kubernetes backend for Tower execution +//! +//! This module provides ExecutionBackend implementation for Kubernetes, supporting: +//! - Pod-based isolation with resource limits +//! - PersistentVolumeClaim-based caching +//! - Service endpoints for long-running apps +//! - Log streaming from pods +//! +//! Only available with the "k8s" feature flag. + +use crate::errors::Error; +use crate::execution::{ + BackendCapabilities, CacheBackend, ExecutionBackend, ExecutionHandle, ExecutionSpec, + ExecutionStatus, LogChannel, LogLine, LogReceiver, LogStream, NetworkingSpec, ServiceEndpoint, +}; + +use async_trait::async_trait; +use chrono::Utc; +use k8s_openapi::api::core::v1::{ + Container, Pod, PodSpec, ResourceRequirements, Service, ServicePort, ServiceSpec, Volume, + VolumeMount, +}; +use kube::{ + api::{Api, DeleteParams, ListParams, LogParams, PostParams}, + runtime::wait::{await_condition, conditions}, + Client, +}; +use std::collections::BTreeMap; +use std::sync::Arc; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::sync::Mutex; +use tokio_util::compat::FuturesAsyncReadCompatExt; + +/// K8sBackend executes apps as Kubernetes Pods +pub struct K8sBackend { + client: Client, + namespace: String, + cache_pv_claim: Option, +} + +impl K8sBackend { + /// Create a new K8sBackend + pub async fn new(namespace: String, cache_pv_claim: Option) -> Result { + let client = Client::try_default() + .await + .map_err(|_| Error::RuntimeStartFailed)?; + + Ok(Self { + client, + namespace, + cache_pv_claim, + }) + } + + /// Build pod spec from execution spec + fn build_pod_spec(&self, spec: &ExecutionSpec) -> Result { + let mut labels = BTreeMap::new(); + labels.insert("app".to_string(), "tower-app".to_string()); + labels.insert("execution-id".to_string(), spec.id.clone()); + + // Build environment variables + let mut env_vars = vec![]; + for (key, value) in &spec.secrets { + env_vars.push(k8s_openapi::api::core::v1::EnvVar { + name: key.clone(), + value: Some(value.clone()), + ..Default::default() + }); + } + for (key, value) in &spec.parameters { + env_vars.push(k8s_openapi::api::core::v1::EnvVar { + name: key.clone(), + value: Some(value.clone()), + ..Default::default() + }); + } + for (key, value) in &spec.env_vars { + env_vars.push(k8s_openapi::api::core::v1::EnvVar { + name: key.clone(), + value: Some(value.clone()), + ..Default::default() + }); + } + + // Build volume mounts for caching + let mut volume_mounts = vec![]; + let mut volumes = vec![]; + + if let CacheBackend::K8sPersistentVolume { pv_claim_name } = &spec.runtime.cache.backend { + // Mount cache PVC + volume_mounts.push(VolumeMount { + name: "cache".to_string(), + mount_path: "/cache".to_string(), + ..Default::default() + }); + volumes.push(Volume { + name: "cache".to_string(), + persistent_volume_claim: Some( + k8s_openapi::api::core::v1::PersistentVolumeClaimVolumeSource { + claim_name: pv_claim_name.clone(), + read_only: Some(false), + }, + ), + ..Default::default() + }); + } + + // Build resource requirements + let mut resource_limits = BTreeMap::new(); + let mut resource_requests = BTreeMap::new(); + + if let Some(cpu) = spec.resources.cpu_millicores { + let cpu_str = format!("{}m", cpu); + resource_limits.insert( + "cpu".to_string(), + k8s_openapi::apimachinery::pkg::api::resource::Quantity(cpu_str.clone()), + ); + resource_requests.insert( + "cpu".to_string(), + k8s_openapi::apimachinery::pkg::api::resource::Quantity(cpu_str), + ); + } + + if let Some(memory) = spec.resources.memory_mb { + let mem_str = format!("{}Mi", memory); + resource_limits.insert( + "memory".to_string(), + k8s_openapi::apimachinery::pkg::api::resource::Quantity(mem_str.clone()), + ); + resource_requests.insert( + "memory".to_string(), + k8s_openapi::apimachinery::pkg::api::resource::Quantity(mem_str), + ); + } + + if let Some(storage) = spec.resources.storage_mb { + let storage_str = format!("{}Mi", storage); + resource_limits.insert( + "ephemeral-storage".to_string(), + k8s_openapi::apimachinery::pkg::api::resource::Quantity(storage_str.clone()), + ); + resource_requests.insert( + "ephemeral-storage".to_string(), + k8s_openapi::apimachinery::pkg::api::resource::Quantity(storage_str), + ); + } + + let resources = ResourceRequirements { + limits: Some(resource_limits), + requests: Some(resource_requests), + ..Default::default() + }; + + // Build container spec + let container = Container { + name: "app".to_string(), + image: Some(spec.runtime.image.clone()), + env: Some(env_vars), + volume_mounts: if volume_mounts.is_empty() { + None + } else { + Some(volume_mounts) + }, + resources: Some(resources), + ..Default::default() + }; + + // Build pod spec + let pod_spec = PodSpec { + containers: vec![container], + volumes: if volumes.is_empty() { + None + } else { + Some(volumes) + }, + restart_policy: Some("Never".to_string()), + ..Default::default() + }; + + Ok(Pod { + metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta { + name: Some(format!("tower-run-{}", spec.id)), + namespace: Some(self.namespace.clone()), + labels: Some(labels), + ..Default::default() + }, + spec: Some(pod_spec), + ..Default::default() + }) + } + + /// Build service spec for networking + fn build_service_spec( + &self, + exec_id: &str, + networking: &NetworkingSpec, + ) -> Result { + let mut labels = BTreeMap::new(); + labels.insert("app".to_string(), "tower-app".to_string()); + labels.insert("execution-id".to_string(), exec_id.to_string()); + + let service_port = ServicePort { + name: Some("http".to_string()), + port: networking.port as i32, + target_port: Some( + k8s_openapi::apimachinery::pkg::util::intstr::IntOrString::Int( + networking.port as i32, + ), + ), + ..Default::default() + }; + + let service_spec = ServiceSpec { + selector: Some(labels.clone()), + ports: Some(vec![service_port]), + type_: Some("ClusterIP".to_string()), + ..Default::default() + }; + + Ok(Service { + metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta { + name: Some( + networking + .service_name + .clone() + .unwrap_or_else(|| format!("tower-svc-{}", exec_id)), + ), + namespace: Some(self.namespace.clone()), + labels: Some(labels), + ..Default::default() + }, + spec: Some(service_spec), + ..Default::default() + }) + } +} + +#[async_trait] +impl ExecutionBackend for K8sBackend { + type Handle = K8sHandle; + + async fn create(&self, spec: ExecutionSpec) -> Result { + let pods: Api = Api::namespaced(self.client.clone(), &self.namespace); + + // Build and create pod + let pod = self.build_pod_spec(&spec)?; + let pod_name = pod.metadata.name.clone().ok_or(Error::RuntimeStartFailed)?; + + pods.create(&PostParams::default(), &pod) + .await + .map_err(|_| Error::RuntimeStartFailed)?; + + // Create service if networking is specified + let service_endpoint = if let Some(networking) = &spec.networking { + if networking.expose_service { + let services: Api = Api::namespaced(self.client.clone(), &self.namespace); + let service = self.build_service_spec(&spec.id, networking)?; + let service_name = service + .metadata + .name + .clone() + .ok_or(Error::RuntimeStartFailed)?; + + services + .create(&PostParams::default(), &service) + .await + .map_err(|_| Error::RuntimeStartFailed)?; + + Some(ServiceEndpoint { + host: format!("{}.{}.svc.cluster.local", service_name, self.namespace), + port: networking.port, + protocol: "http".to_string(), + url: Some(format!( + "http://{}.{}.svc.cluster.local:{}", + service_name, self.namespace, networking.port + )), + }) + } else { + None + } + } else { + None + }; + + Ok(K8sHandle { + id: spec.id, + pod_name, + namespace: self.namespace.clone(), + client: self.client.clone(), + service_endpoint: Arc::new(Mutex::new(service_endpoint)), + }) + } + + fn capabilities(&self) -> BackendCapabilities { + BackendCapabilities { + name: "k8s".to_string(), + supports_persistent_cache: true, + supports_prewarming: true, + supports_network_isolation: true, + supports_service_endpoints: true, + typical_cold_start_ms: 5000, // ~5s for image pull + pod start + typical_warm_start_ms: 1000, // ~1s with cached image + max_concurrent_executions: None, // Limited by cluster capacity + } + } + + async fn cleanup(&self) -> Result<(), Error> { + // No global cleanup needed for K8s backend + Ok(()) + } +} + +/// K8sHandle provides lifecycle management for a Kubernetes Pod execution +pub struct K8sHandle { + id: String, + pod_name: String, + namespace: String, + client: Client, + service_endpoint: Arc>>, +} + +#[async_trait] +impl ExecutionHandle for K8sHandle { + fn id(&self) -> &str { + &self.id + } + + async fn status(&self) -> Result { + let pods: Api = Api::namespaced(self.client.clone(), &self.namespace); + + let pod = pods + .get(&self.pod_name) + .await + .map_err(|_| Error::NoRunningApp)?; + + let phase = pod + .status + .and_then(|s| s.phase) + .unwrap_or_else(|| "Unknown".to_string()); + + Ok(match phase.as_str() { + "Pending" => ExecutionStatus::Preparing, + "Running" => ExecutionStatus::Running, + "Succeeded" => ExecutionStatus::Succeeded, + "Failed" => ExecutionStatus::Failed { exit_code: 1 }, + _ => ExecutionStatus::Unknown, + }) + } + + async fn logs(&self) -> Result { + let pods: Api = Api::namespaced(self.client.clone(), &self.namespace); + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + + let pod_name = self.pod_name.clone(); + let pods_clone = pods.clone(); + + tokio::spawn(async move { + // Wait for pod to be running before streaming logs + if let Ok(_) = + await_condition(pods_clone.clone(), &pod_name, conditions::is_pod_running()).await + { + let log_params = LogParams { + follow: true, + ..Default::default() + }; + + if let Ok(logs) = pods_clone.log_stream(&pod_name, &log_params).await { + // Convert futures AsyncBufRead to tokio AsyncRead + let compat_logs = logs.compat(); + let mut reader = BufReader::new(compat_logs).lines(); + while let Ok(Some(line)) = reader.next_line().await { + let log_line = LogLine { + timestamp: Utc::now(), + stream: LogStream::Stdout, // K8s combines stdout/stderr + channel: LogChannel::Program, + content: line, + }; + if tx.send(log_line).is_err() { + break; + } + } + } + } + }); + + Ok(rx) + } + + async fn terminate(&mut self) -> Result<(), Error> { + let pods: Api = Api::namespaced(self.client.clone(), &self.namespace); + + pods.delete(&self.pod_name, &DeleteParams::default()) + .await + .map_err(|_| Error::TerminateFailed)?; + + Ok(()) + } + + async fn kill(&mut self) -> Result<(), Error> { + // For K8s, kill is the same as terminate (pod deletion) + self.terminate().await + } + + async fn wait_for_completion(&self) -> Result { + let pods: Api = Api::namespaced(self.client.clone(), &self.namespace); + + // Wait for pod to reach terminal state + await_condition(pods.clone(), &self.pod_name, |obj: Option<&Pod>| { + obj.and_then(|pod| pod.status.as_ref()) + .and_then(|status| status.phase.as_ref()) + .map(|phase| phase == "Succeeded" || phase == "Failed") + .unwrap_or(false) + }) + .await + .map_err(|_| Error::Timeout)?; + + self.status().await + } + + async fn service_endpoint(&self) -> Result, Error> { + let endpoint = self.service_endpoint.lock().await; + Ok(endpoint.clone()) + } + + async fn cleanup(&mut self) -> Result<(), Error> { + // Delete pod + self.terminate().await?; + + // Delete service if it exists + if let Some(endpoint) = self.service_endpoint.lock().await.as_ref() { + let services: Api = Api::namespaced(self.client.clone(), &self.namespace); + // Extract service name from hostname + let service_name = endpoint.host.split('.').next().unwrap_or("unknown"); + let _ = services + .delete(service_name, &DeleteParams::default()) + .await; + } + + Ok(()) + } +} diff --git a/crates/tower-runtime/src/lib.rs b/crates/tower-runtime/src/lib.rs index 195c3e26..f32cb894 100644 --- a/crates/tower-runtime/src/lib.rs +++ b/crates/tower-runtime/src/lib.rs @@ -8,8 +8,12 @@ use tower_package::Package; use tower_telemetry::debug; pub mod errors; +pub mod execution; pub mod local; +#[cfg(feature = "k8s")] +pub mod k8s; + use errors::Error; #[derive(Copy, Clone)] diff --git a/crates/tower-runtime/src/local.rs b/crates/tower-runtime/src/local.rs index ee4968e2..2045a54f 100644 --- a/crates/tower-runtime/src/local.rs +++ b/crates/tower-runtime/src/local.rs @@ -572,3 +572,179 @@ async fn drain_output( fn is_bash_package(package: &Package) -> bool { return package.manifest.invoke.ends_with(".sh"); } + +// ============================================================================ +// LocalBackend - ExecutionBackend implementation for local subprocess execution +// ============================================================================ + +use crate::execution::{ + BackendCapabilities, BundleRef, CacheBackend, ExecutionBackend, ExecutionHandle, ExecutionSpec, + ExecutionStatus, LogChannel, LogLine, LogReceiver, LogStream, ServiceEndpoint, +}; +use async_trait::async_trait; +use std::sync::Arc; + +/// LocalBackend executes apps as local subprocesses +pub struct LocalBackend { + /// Optional default cache directory to use + cache_dir: Option, +} + +impl LocalBackend { + pub fn new(cache_dir: Option) -> Self { + Self { cache_dir } + } +} + +#[async_trait] +impl ExecutionBackend for LocalBackend { + type Handle = LocalHandle; + + async fn create(&self, spec: ExecutionSpec) -> Result { + // Convert ExecutionSpec to StartOptions for LocalApp + let (output_sender, output_receiver) = tokio::sync::mpsc::unbounded_channel(); + + // Get cache_dir from spec or use backend default + let cache_dir = match &spec.runtime.cache.backend { + CacheBackend::Local { cache_dir } => Some(cache_dir.clone()), + _ => self.cache_dir.clone(), + }; + + let opts = StartOptions { + ctx: spec.telemetry_ctx, + package: match spec.bundle { + BundleRef::Local { path } => Package::from_unpacked_path(path).await, + _ => return Err(Error::NotImplemented), // Remote bundles not yet supported + }, + cwd: None, // LocalApp determines cwd from package + environment: spec.environment, + secrets: spec.secrets, + parameters: spec.parameters, + env_vars: spec.env_vars, + output_sender: output_sender.clone(), + cache_dir, + }; + + // Start the LocalApp + let app = LocalApp::start(opts).await?; + + Ok(LocalHandle { + id: spec.id, + app: Arc::new(Mutex::new(app)), + output_receiver: Arc::new(Mutex::new(output_receiver)), + }) + } + + fn capabilities(&self) -> BackendCapabilities { + BackendCapabilities { + name: "local".to_string(), + supports_persistent_cache: true, + supports_prewarming: false, + supports_network_isolation: false, + supports_service_endpoints: false, + typical_cold_start_ms: 1000, // ~1s for venv + sync + typical_warm_start_ms: 100, // ~100ms with warm cache + max_concurrent_executions: None, // Limited by system resources + } + } + + async fn cleanup(&self) -> Result<(), Error> { + // Nothing to cleanup for local backend + Ok(()) + } +} + +/// LocalHandle provides lifecycle management for a local subprocess execution +pub struct LocalHandle { + id: String, + app: Arc>, + output_receiver: Arc>, +} + +#[async_trait] +impl ExecutionHandle for LocalHandle { + fn id(&self) -> &str { + &self.id + } + + async fn status(&self) -> Result { + let app = self.app.lock().await; + let status = app.status().await?; + + Ok(match status { + Status::None => ExecutionStatus::Preparing, + Status::Running => ExecutionStatus::Running, + Status::Exited => ExecutionStatus::Succeeded, + Status::Crashed { code } => { + if code == -1 { + ExecutionStatus::Terminated + } else { + ExecutionStatus::Failed { exit_code: code } + } + } + }) + } + + async fn logs(&self) -> Result { + // Create a new channel for log streaming + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + + // Spawn a task to convert Output to LogLine + let output_receiver = self.output_receiver.clone(); + tokio::spawn(async move { + let mut receiver = output_receiver.lock().await; + while let Some(output) = receiver.recv().await { + let log_line = LogLine { + timestamp: output.time, + stream: match output.fd { + FD::Stdout => LogStream::Stdout, + FD::Stderr => LogStream::Stderr, + }, + channel: match output.channel { + Channel::Setup => LogChannel::Setup, + Channel::Program => LogChannel::Program, + }, + content: output.line, + }; + if tx.send(log_line).is_err() { + break; // Receiver dropped + } + } + }); + + Ok(rx) + } + + async fn terminate(&mut self) -> Result<(), Error> { + let mut app = self.app.lock().await; + app.terminate().await + } + + async fn kill(&mut self) -> Result<(), Error> { + // For local processes, kill is the same as terminate + self.terminate().await + } + + async fn wait_for_completion(&self) -> Result { + loop { + let status = self.status().await?; + match status { + ExecutionStatus::Preparing | ExecutionStatus::Running => { + tokio::time::sleep(Duration::from_millis(100)).await; + } + _ => return Ok(status), + } + } + } + + async fn service_endpoint(&self) -> Result, Error> { + // Local backend doesn't support service endpoints + Ok(None) + } + + async fn cleanup(&mut self) -> Result<(), Error> { + // Ensure the app is terminated + self.terminate().await?; + Ok(()) + } +} From 64c8956133ea2e09aa66413bedf209583435afa4 Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Mon, 12 Jan 2026 18:43:05 +0100 Subject: [PATCH 02/21] Working k8s backend --- crates/tower-runtime/src/k8s.rs | 211 ++++++++++++++++++++++++++++++-- 1 file changed, 200 insertions(+), 11 deletions(-) diff --git a/crates/tower-runtime/src/k8s.rs b/crates/tower-runtime/src/k8s.rs index 356797ef..f95bb759 100644 --- a/crates/tower-runtime/src/k8s.rs +++ b/crates/tower-runtime/src/k8s.rs @@ -10,19 +10,19 @@ use crate::errors::Error; use crate::execution::{ - BackendCapabilities, CacheBackend, ExecutionBackend, ExecutionHandle, ExecutionSpec, + BackendCapabilities, BundleRef, CacheBackend, ExecutionBackend, ExecutionHandle, ExecutionSpec, ExecutionStatus, LogChannel, LogLine, LogReceiver, LogStream, NetworkingSpec, ServiceEndpoint, }; use async_trait::async_trait; use chrono::Utc; use k8s_openapi::api::core::v1::{ - Container, Pod, PodSpec, ResourceRequirements, Service, ServicePort, ServiceSpec, Volume, - VolumeMount, + ConfigMap, Container, Pod, PodSpec, ResourceRequirements, Service, ServicePort, ServiceSpec, + Volume, VolumeMount, }; use kube::{ - api::{Api, DeleteParams, ListParams, LogParams, PostParams}, - runtime::wait::{await_condition, conditions}, + api::{Api, DeleteParams, LogParams, PostParams}, + runtime::wait::await_condition, Client, }; use std::collections::BTreeMap; @@ -53,7 +53,11 @@ impl K8sBackend { } /// Build pod spec from execution spec - fn build_pod_spec(&self, spec: &ExecutionSpec) -> Result { + fn build_pod_spec( + &self, + spec: &ExecutionSpec, + path_mapping: &BTreeMap, + ) -> Result { let mut labels = BTreeMap::new(); labels.insert("app".to_string(), "tower-app".to_string()); labels.insert("execution-id".to_string(), spec.id.clone()); @@ -151,17 +155,54 @@ impl K8sBackend { ..Default::default() }; + // Add bundle volume mount + volume_mounts.push(VolumeMount { + name: "bundle".to_string(), + mount_path: "/app".to_string(), + read_only: Some(true), + ..Default::default() + }); + + // Build items array to map ConfigMap keys to their original paths + // e.g., "app__task.py" -> "app/task.py" + let items: Vec = path_mapping + .iter() + .map( + |(sanitized_key, original_path)| k8s_openapi::api::core::v1::KeyToPath { + key: sanitized_key.clone(), + path: original_path.clone(), + mode: Some(0o755), + }, + ) + .collect(); + + // Bundle will be provided as a ConfigMap (created separately) + volumes.push(Volume { + name: "bundle".to_string(), + config_map: Some(k8s_openapi::api::core::v1::ConfigMapVolumeSource { + name: format!("bundle-{}", spec.id), + default_mode: Some(0o755), + items: Some(items), + ..Default::default() + }), + ..Default::default() + }); + // Build container spec + // Note: In K8s, 'command' = entrypoint, 'args' = command let container = Container { name: "app".to_string(), image: Some(spec.runtime.image.clone()), env: Some(env_vars), + command: spec.runtime.entrypoint.clone(), // K8s command = entrypoint + args: spec.runtime.command.clone(), // K8s args = command volume_mounts: if volume_mounts.is_empty() { None } else { Some(volume_mounts) }, resources: Some(resources), + working_dir: Some("/app".to_string()), ..Default::default() }; @@ -189,6 +230,134 @@ impl K8sBackend { }) } + /// Create ConfigMap with bundle contents + /// Returns a mapping of sanitized keys to original paths for volume mounting + async fn create_bundle_configmap( + &self, + spec: &ExecutionSpec, + ) -> Result, Error> { + use k8s_openapi::api::core::v1::ConfigMap; + use std::collections::BTreeMap; + + let configmaps: Api = Api::namespaced(self.client.clone(), &self.namespace); + + // Get bundle path + let bundle_path = match &spec.bundle { + BundleRef::Local { path } => path, + _ => return Err(Error::NotImplemented), // Only Local bundles supported for now + }; + + // Recursively read ALL files from the bundle directory + let mut data = BTreeMap::new(); + let mut binary_data = BTreeMap::new(); + let mut path_mapping = BTreeMap::new(); // sanitized_key -> original_path + + Self::walk_directory( + &bundle_path, + &bundle_path, + &mut data, + &mut binary_data, + &mut path_mapping, + ) + .await?; + + if data.is_empty() && binary_data.is_empty() { + return Err(Error::RuntimeStartFailed); // No files found + } + + let configmap = ConfigMap { + metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta { + name: Some(format!("bundle-{}", spec.id)), + namespace: Some(self.namespace.clone()), + ..Default::default() + }, + data: if !data.is_empty() { Some(data) } else { None }, + binary_data: if !binary_data.is_empty() { + Some(binary_data) + } else { + None + }, + ..Default::default() + }; + + configmaps + .create(&PostParams::default(), &configmap) + .await + .map_err(|_| Error::RuntimeStartFailed)?; + + Ok(path_mapping) + } + + /// Sanitize a file path to be a valid ConfigMap key + /// Replaces '/' with '__' to comply with K8s key restrictions: [-._a-zA-Z0-9]+ + fn sanitize_configmap_key(path: &str) -> String { + path.replace('/', "__") + } + + /// Recursively walk directory and collect all files + async fn walk_directory( + current_path: &std::path::Path, + base_path: &std::path::Path, + text_data: &mut BTreeMap, + binary_data: &mut BTreeMap, + path_mapping: &mut BTreeMap, + ) -> Result<(), Error> { + use tokio::fs; + + let mut entries = fs::read_dir(current_path) + .await + .map_err(|_| Error::RuntimeStartFailed)?; + + while let Some(entry) = entries + .next_entry() + .await + .map_err(|_| Error::RuntimeStartFailed)? + { + let path = entry.path(); + + if path.is_dir() { + // Recursively process subdirectories + Box::pin(Self::walk_directory( + &path, + base_path, + text_data, + binary_data, + path_mapping, + )) + .await?; + } else if path.is_file() { + // Get relative path from base (e.g., "app/task.py") + let relative_path = path + .strip_prefix(base_path) + .map_err(|_| Error::RuntimeStartFailed)? + .to_str() + .ok_or(Error::RuntimeStartFailed)? + .to_string(); + + // Sanitize the key for ConfigMap (e.g., "app/task.py" -> "app__task.py") + let sanitized_key = Self::sanitize_configmap_key(&relative_path); + + // Store mapping for volume mount reconstruction + path_mapping.insert(sanitized_key.clone(), relative_path.clone()); + + // Try reading as text first + match fs::read_to_string(&path).await { + Ok(contents) => { + text_data.insert(sanitized_key, contents); + } + Err(_) => { + // If not text, read as binary + if let Ok(contents) = fs::read(&path).await { + binary_data.insert(sanitized_key, k8s_openapi::ByteString(contents)); + } + } + } + } + } + + Ok(()) + } + /// Build service spec for networking fn build_service_spec( &self, @@ -242,8 +411,11 @@ impl ExecutionBackend for K8sBackend { async fn create(&self, spec: ExecutionSpec) -> Result { let pods: Api = Api::namespaced(self.client.clone(), &self.namespace); - // Build and create pod - let pod = self.build_pod_spec(&spec)?; + // Create ConfigMap with bundle contents and get path mapping + let path_mapping = self.create_bundle_configmap(&spec).await?; + + // Build and create pod with path mapping for volume items + let pod = self.build_pod_spec(&spec, &path_mapping)?; let pod_name = pod.metadata.name.clone().ok_or(Error::RuntimeStartFailed)?; pods.create(&PostParams::default(), &pod) @@ -355,9 +527,19 @@ impl ExecutionHandle for K8sHandle { let pods_clone = pods.clone(); tokio::spawn(async move { - // Wait for pod to be running before streaming logs - if let Ok(_) = - await_condition(pods_clone.clone(), &pod_name, conditions::is_pod_running()).await + // Wait for pod to have containers created (Running, Succeeded, or Failed) + // This ensures we can stream logs even if the pod crashes + let condition = await_condition(pods_clone.clone(), &pod_name, |obj: Option<&Pod>| { + obj.and_then(|pod| pod.status.as_ref()) + .and_then(|status| status.phase.as_ref()) + .map(|phase| phase == "Running" || phase == "Succeeded" || phase == "Failed") + .unwrap_or(false) + }); + + // Wait with a timeout + if tokio::time::timeout(std::time::Duration::from_secs(60), condition) + .await + .is_ok() { let log_params = LogParams { follow: true, @@ -426,6 +608,13 @@ impl ExecutionHandle for K8sHandle { // Delete pod self.terminate().await?; + // Delete ConfigMap with bundle + let configmaps: Api = Api::namespaced(self.client.clone(), &self.namespace); + let configmap_name = format!("bundle-{}", self.id); + let _ = configmaps + .delete(&configmap_name, &DeleteParams::default()) + .await; + // Delete service if it exists if let Some(endpoint) = self.service_endpoint.lock().await.as_ref() { let services: Api = Api::namespaced(self.client.clone(), &self.namespace); From 079982ae51ca9ebe03e2f40e3ce210c6fcae381a Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Mon, 12 Jan 2026 18:49:10 +0100 Subject: [PATCH 03/21] move k8s backend to tower-runner --- Cargo.lock | 660 +------------------------------- crates/tower-runtime/Cargo.toml | 7 - crates/tower-runtime/src/k8s.rs | 630 ------------------------------ crates/tower-runtime/src/lib.rs | 3 - 4 files changed, 13 insertions(+), 1287 deletions(-) delete mode 100644 crates/tower-runtime/src/k8s.rs diff --git a/Cargo.lock b/Cargo.lock index ce159829..a69ea5b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,19 +52,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.3", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -74,12 +61,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "android-tzdata" version = "0.1.1" @@ -151,18 +132,6 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" -[[package]] -name = "async-broadcast" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" -dependencies = [ - "event-listener", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - [[package]] name = "async-compression" version = "0.4.27" @@ -285,17 +254,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "backoff" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" -dependencies = [ - "getrandom 0.2.16", - "instant", - "rand 0.8.5", -] - [[package]] name = "backtrace" version = "0.3.75" @@ -311,12 +269,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -423,7 +375,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -537,15 +489,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "config" version = "0.3.39" @@ -585,16 +528,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[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" @@ -668,7 +601,7 @@ name = "crypto" version = "0.3.39" dependencies = [ "aes-gcm", - "base64 0.22.1", + "base64", "pem", "rand 0.8.5", "rsa", @@ -878,17 +811,6 @@ dependencies = [ "serde", ] -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "derive_more" version = "0.99.20" @@ -1026,27 +948,6 @@ dependencies = [ "str-buf", ] -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "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 = "eventsource-stream" version = "0.2.3" @@ -1097,15 +998,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "fluent-uri" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "fnv" version = "1.0.7" @@ -1335,46 +1227,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] - [[package]] name = "hashbrown" version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" -[[package]] -name = "headers" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" -dependencies = [ - "base64 0.22.1", - "bytes", - "headers-core", - "http", - "httpdate", - "mime", - "sha1", -] - -[[package]] -name = "headers-core" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" -dependencies = [ - "http", -] - [[package]] name = "heck" version = "0.4.1" @@ -1393,15 +1251,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "http" version = "1.3.1" @@ -1474,26 +1323,6 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-http-proxy" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ad4b0a1e37510028bc4ba81d0e38d239c39671b0f0ce9e02dfa93a8133f7c08" -dependencies = [ - "bytes", - "futures-util", - "headers", - "http", - "hyper", - "hyper-rustls", - "hyper-util", - "pin-project-lite", - "rustls-native-certs 0.7.3", - "tokio", - "tokio-rustls", - "tower-service", -] - [[package]] name = "hyper-rustls" version = "0.27.7" @@ -1503,9 +1332,7 @@ dependencies = [ "http", "hyper", "hyper-util", - "log", "rustls", - "rustls-native-certs 0.8.3", "rustls-pki-types", "tokio", "tokio-rustls", @@ -1513,26 +1340,13 @@ dependencies = [ "webpki-roots", ] -[[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.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", "futures-core", @@ -1753,15 +1567,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "inventory" version = "0.3.21" @@ -1851,167 +1656,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json-patch" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1fb8864823fad91877e6caea0baca82e49e8db50f8e5c9f9a453e27d3330fc" -dependencies = [ - "jsonptr", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "jsonpath-rust" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d8fe85bd70ff715f31ce8c739194b423d79811a19602115d611a3ec85d6200" -dependencies = [ - "lazy_static", - "once_cell", - "pest", - "pest_derive", - "regex", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "jsonptr" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c6e529149475ca0b2820835d3dce8fcc41c6b943ca608d32f35b449255e4627" -dependencies = [ - "fluent-uri", - "serde", - "serde_json", -] - -[[package]] -name = "k8s-openapi" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8847402328d8301354c94d605481f25a6bdc1ed65471fd96af8eca71141b13" -dependencies = [ - "base64 0.22.1", - "chrono", - "serde", - "serde-value", - "serde_json", -] - -[[package]] -name = "kube" -version = "0.95.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa21063c854820a77c5d7f8deeb7ffa55246d8304e4bcd8cce2956752c6604f8" -dependencies = [ - "k8s-openapi", - "kube-client", - "kube-core", - "kube-derive", - "kube-runtime", -] - -[[package]] -name = "kube-client" -version = "0.95.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c2355f5c9d8a11900e71a6fe1e47abd5ec45bf971eb4b162ffe97b46db9bb7" -dependencies = [ - "base64 0.22.1", - "bytes", - "chrono", - "either", - "futures", - "home", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-http-proxy", - "hyper-rustls", - "hyper-timeout", - "hyper-util", - "jsonpath-rust", - "k8s-openapi", - "kube-core", - "pem", - "rustls", - "rustls-pemfile", - "secrecy", - "serde", - "serde_json", - "serde_yaml", - "thiserror 1.0.69", - "tokio", - "tokio-util", - "tower 0.4.13", - "tower-http 0.5.2", - "tracing", -] - -[[package]] -name = "kube-core" -version = "0.95.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3030bd91c9db544a50247e7d48d7db9cf633c172732dce13351854526b1e666" -dependencies = [ - "chrono", - "form_urlencoded", - "http", - "json-patch", - "k8s-openapi", - "schemars 0.8.22", - "serde", - "serde-value", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "kube-derive" -version = "0.95.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa98be978eddd70a773aa8e86346075365bfb7eb48783410852dbf7cb57f0c27" -dependencies = [ - "darling 0.20.11", - "proc-macro2", - "quote", - "serde_json", - "syn 2.0.104", -] - -[[package]] -name = "kube-runtime" -version = "0.95.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5895cb8aa641ac922408f128b935652b34c2995f16ad7db0984f6caa50217914" -dependencies = [ - "ahash", - "async-broadcast", - "async-stream", - "async-trait", - "backoff", - "derivative", - "futures", - "hashbrown 0.14.5", - "json-patch", - "jsonptr", - "k8s-openapi", - "kube-client", - "parking_lot", - "pin-project", - "serde", - "serde_json", - "thiserror 1.0.69", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "lazy-regex" version = "3.4.1" @@ -2373,33 +2017,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-probe" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" - [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "ordered-float" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] - [[package]] name = "overload" version = "0.1.1" @@ -2474,7 +2097,7 @@ version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" dependencies = [ - "base64 0.22.1", + "base64", "serde", ] @@ -2493,49 +2116,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "pest" -version = "2.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "pest_meta" -version = "2.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" -dependencies = [ - "pest", - "sha2", -] - [[package]] name = "pin-project" version = "1.1.10" @@ -2893,7 +2473,7 @@ version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-core", "futures-util", @@ -2919,7 +2499,7 @@ dependencies = [ "tokio-rustls", "tokio-util", "tower 0.5.2", - "tower-http 0.6.6", + "tower-http", "tower-service", "url", "wasm-bindgen", @@ -2966,7 +2546,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2faf35b7d3c4b7f8c21c45bb014011b32a0ce6444bf6094da04daab01a8c3c34" dependencies = [ "axum", - "base64 0.22.1", + "base64", "bytes", "chrono", "futures", @@ -3088,7 +2668,6 @@ version = "0.23.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" dependencies = [ - "log", "once_cell", "ring", "rustls-pki-types", @@ -3097,40 +2676,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" -dependencies = [ - "openssl-probe 0.1.6", - "rustls-pemfile", - "rustls-pki-types", - "schannel", - "security-framework 2.11.1", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" -dependencies = [ - "openssl-probe 0.2.0", - "rustls-pki-types", - "schannel", - "security-framework 3.5.1", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.12.0" @@ -3197,27 +2742,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "schemars" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" -dependencies = [ - "dyn-clone", - "schemars_derive 0.8.22", - "serde", - "serde_json", -] - [[package]] name = "schemars" version = "0.9.0" @@ -3239,23 +2763,11 @@ dependencies = [ "chrono", "dyn-clone", "ref-cast", - "schemars_derive 1.0.4", + "schemars_derive", "serde", "serde_json", ] -[[package]] -name = "schemars_derive" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.104", -] - [[package]] name = "schemars_derive" version = "1.0.4" @@ -3286,52 +2798,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "secrecy" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" -dependencies = [ - "serde", - "zeroize", -] - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.9.1", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" -dependencies = [ - "bitflags 2.9.1", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "serde" version = "1.0.219" @@ -3341,16 +2807,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float", - "serde", -] - [[package]] name = "serde_derive" version = "1.0.219" @@ -3433,7 +2889,7 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" dependencies = [ - "base64 0.22.1", + "base64", "chrono", "hex", "indexmap 1.9.3", @@ -3459,30 +2915,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap 2.10.0", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sha2" version = "0.10.9" @@ -4018,7 +3450,6 @@ dependencies = [ "futures-io", "futures-sink", "pin-project-lite", - "slab", "tokio", ] @@ -4072,23 +3503,6 @@ dependencies = [ "tower-cmd", ] -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower" version = "0.5.2" @@ -4163,25 +3577,6 @@ dependencies = [ "webbrowser", ] -[[package]] -name = "tower-http" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" -dependencies = [ - "base64 0.21.7", - "bitflags 2.9.1", - "bytes", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower-http" version = "0.6.6" @@ -4232,8 +3627,6 @@ dependencies = [ "async-trait", "chrono", "config", - "k8s-openapi", - "kube", "nix 0.30.1", "snafu", "tokio", @@ -4404,12 +3797,6 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - [[package]] name = "unicase" version = "2.8.1" @@ -4456,12 +3843,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "untrusted" version = "0.9.0" @@ -4660,7 +4041,7 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aaf4f3c0ba838e82b4e5ccc4157003fb8c324ee24c058470ffb82820becbde98" dependencies = [ - "core-foundation 0.10.1", + "core-foundation", "jni", "log", "ndk-context", @@ -4718,7 +4099,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.1.3", + "windows-link", "windows-result", "windows-strings", ] @@ -4751,19 +4132,13 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -4772,7 +4147,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -4820,15 +4195,6 @@ dependencies = [ "windows-targets 0.53.2", ] -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-targets" version = "0.42.2" diff --git a/crates/tower-runtime/Cargo.toml b/crates/tower-runtime/Cargo.toml index 237ae18f..aa85e75f 100644 --- a/crates/tower-runtime/Cargo.toml +++ b/crates/tower-runtime/Cargo.toml @@ -18,12 +18,5 @@ tower-telemetry = { workspace = true } tower-uv = { workspace = true } uuid = { workspace = true } -# Optional dependencies for K8s backend -kube = { workspace = true, optional = true } -k8s-openapi = { workspace = true, optional = true } - -[features] -k8s = ["kube", "k8s-openapi"] - [dev-dependencies] config = { workspace = true } diff --git a/crates/tower-runtime/src/k8s.rs b/crates/tower-runtime/src/k8s.rs deleted file mode 100644 index f95bb759..00000000 --- a/crates/tower-runtime/src/k8s.rs +++ /dev/null @@ -1,630 +0,0 @@ -//! Kubernetes backend for Tower execution -//! -//! This module provides ExecutionBackend implementation for Kubernetes, supporting: -//! - Pod-based isolation with resource limits -//! - PersistentVolumeClaim-based caching -//! - Service endpoints for long-running apps -//! - Log streaming from pods -//! -//! Only available with the "k8s" feature flag. - -use crate::errors::Error; -use crate::execution::{ - BackendCapabilities, BundleRef, CacheBackend, ExecutionBackend, ExecutionHandle, ExecutionSpec, - ExecutionStatus, LogChannel, LogLine, LogReceiver, LogStream, NetworkingSpec, ServiceEndpoint, -}; - -use async_trait::async_trait; -use chrono::Utc; -use k8s_openapi::api::core::v1::{ - ConfigMap, Container, Pod, PodSpec, ResourceRequirements, Service, ServicePort, ServiceSpec, - Volume, VolumeMount, -}; -use kube::{ - api::{Api, DeleteParams, LogParams, PostParams}, - runtime::wait::await_condition, - Client, -}; -use std::collections::BTreeMap; -use std::sync::Arc; -use tokio::io::{AsyncBufReadExt, BufReader}; -use tokio::sync::Mutex; -use tokio_util::compat::FuturesAsyncReadCompatExt; - -/// K8sBackend executes apps as Kubernetes Pods -pub struct K8sBackend { - client: Client, - namespace: String, - cache_pv_claim: Option, -} - -impl K8sBackend { - /// Create a new K8sBackend - pub async fn new(namespace: String, cache_pv_claim: Option) -> Result { - let client = Client::try_default() - .await - .map_err(|_| Error::RuntimeStartFailed)?; - - Ok(Self { - client, - namespace, - cache_pv_claim, - }) - } - - /// Build pod spec from execution spec - fn build_pod_spec( - &self, - spec: &ExecutionSpec, - path_mapping: &BTreeMap, - ) -> Result { - let mut labels = BTreeMap::new(); - labels.insert("app".to_string(), "tower-app".to_string()); - labels.insert("execution-id".to_string(), spec.id.clone()); - - // Build environment variables - let mut env_vars = vec![]; - for (key, value) in &spec.secrets { - env_vars.push(k8s_openapi::api::core::v1::EnvVar { - name: key.clone(), - value: Some(value.clone()), - ..Default::default() - }); - } - for (key, value) in &spec.parameters { - env_vars.push(k8s_openapi::api::core::v1::EnvVar { - name: key.clone(), - value: Some(value.clone()), - ..Default::default() - }); - } - for (key, value) in &spec.env_vars { - env_vars.push(k8s_openapi::api::core::v1::EnvVar { - name: key.clone(), - value: Some(value.clone()), - ..Default::default() - }); - } - - // Build volume mounts for caching - let mut volume_mounts = vec![]; - let mut volumes = vec![]; - - if let CacheBackend::K8sPersistentVolume { pv_claim_name } = &spec.runtime.cache.backend { - // Mount cache PVC - volume_mounts.push(VolumeMount { - name: "cache".to_string(), - mount_path: "/cache".to_string(), - ..Default::default() - }); - volumes.push(Volume { - name: "cache".to_string(), - persistent_volume_claim: Some( - k8s_openapi::api::core::v1::PersistentVolumeClaimVolumeSource { - claim_name: pv_claim_name.clone(), - read_only: Some(false), - }, - ), - ..Default::default() - }); - } - - // Build resource requirements - let mut resource_limits = BTreeMap::new(); - let mut resource_requests = BTreeMap::new(); - - if let Some(cpu) = spec.resources.cpu_millicores { - let cpu_str = format!("{}m", cpu); - resource_limits.insert( - "cpu".to_string(), - k8s_openapi::apimachinery::pkg::api::resource::Quantity(cpu_str.clone()), - ); - resource_requests.insert( - "cpu".to_string(), - k8s_openapi::apimachinery::pkg::api::resource::Quantity(cpu_str), - ); - } - - if let Some(memory) = spec.resources.memory_mb { - let mem_str = format!("{}Mi", memory); - resource_limits.insert( - "memory".to_string(), - k8s_openapi::apimachinery::pkg::api::resource::Quantity(mem_str.clone()), - ); - resource_requests.insert( - "memory".to_string(), - k8s_openapi::apimachinery::pkg::api::resource::Quantity(mem_str), - ); - } - - if let Some(storage) = spec.resources.storage_mb { - let storage_str = format!("{}Mi", storage); - resource_limits.insert( - "ephemeral-storage".to_string(), - k8s_openapi::apimachinery::pkg::api::resource::Quantity(storage_str.clone()), - ); - resource_requests.insert( - "ephemeral-storage".to_string(), - k8s_openapi::apimachinery::pkg::api::resource::Quantity(storage_str), - ); - } - - let resources = ResourceRequirements { - limits: Some(resource_limits), - requests: Some(resource_requests), - ..Default::default() - }; - - // Add bundle volume mount - volume_mounts.push(VolumeMount { - name: "bundle".to_string(), - mount_path: "/app".to_string(), - read_only: Some(true), - ..Default::default() - }); - - // Build items array to map ConfigMap keys to their original paths - // e.g., "app__task.py" -> "app/task.py" - let items: Vec = path_mapping - .iter() - .map( - |(sanitized_key, original_path)| k8s_openapi::api::core::v1::KeyToPath { - key: sanitized_key.clone(), - path: original_path.clone(), - mode: Some(0o755), - }, - ) - .collect(); - - // Bundle will be provided as a ConfigMap (created separately) - volumes.push(Volume { - name: "bundle".to_string(), - config_map: Some(k8s_openapi::api::core::v1::ConfigMapVolumeSource { - name: format!("bundle-{}", spec.id), - default_mode: Some(0o755), - items: Some(items), - ..Default::default() - }), - ..Default::default() - }); - - // Build container spec - // Note: In K8s, 'command' = entrypoint, 'args' = command - let container = Container { - name: "app".to_string(), - image: Some(spec.runtime.image.clone()), - env: Some(env_vars), - command: spec.runtime.entrypoint.clone(), // K8s command = entrypoint - args: spec.runtime.command.clone(), // K8s args = command - volume_mounts: if volume_mounts.is_empty() { - None - } else { - Some(volume_mounts) - }, - resources: Some(resources), - working_dir: Some("/app".to_string()), - ..Default::default() - }; - - // Build pod spec - let pod_spec = PodSpec { - containers: vec![container], - volumes: if volumes.is_empty() { - None - } else { - Some(volumes) - }, - restart_policy: Some("Never".to_string()), - ..Default::default() - }; - - Ok(Pod { - metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta { - name: Some(format!("tower-run-{}", spec.id)), - namespace: Some(self.namespace.clone()), - labels: Some(labels), - ..Default::default() - }, - spec: Some(pod_spec), - ..Default::default() - }) - } - - /// Create ConfigMap with bundle contents - /// Returns a mapping of sanitized keys to original paths for volume mounting - async fn create_bundle_configmap( - &self, - spec: &ExecutionSpec, - ) -> Result, Error> { - use k8s_openapi::api::core::v1::ConfigMap; - use std::collections::BTreeMap; - - let configmaps: Api = Api::namespaced(self.client.clone(), &self.namespace); - - // Get bundle path - let bundle_path = match &spec.bundle { - BundleRef::Local { path } => path, - _ => return Err(Error::NotImplemented), // Only Local bundles supported for now - }; - - // Recursively read ALL files from the bundle directory - let mut data = BTreeMap::new(); - let mut binary_data = BTreeMap::new(); - let mut path_mapping = BTreeMap::new(); // sanitized_key -> original_path - - Self::walk_directory( - &bundle_path, - &bundle_path, - &mut data, - &mut binary_data, - &mut path_mapping, - ) - .await?; - - if data.is_empty() && binary_data.is_empty() { - return Err(Error::RuntimeStartFailed); // No files found - } - - let configmap = ConfigMap { - metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta { - name: Some(format!("bundle-{}", spec.id)), - namespace: Some(self.namespace.clone()), - ..Default::default() - }, - data: if !data.is_empty() { Some(data) } else { None }, - binary_data: if !binary_data.is_empty() { - Some(binary_data) - } else { - None - }, - ..Default::default() - }; - - configmaps - .create(&PostParams::default(), &configmap) - .await - .map_err(|_| Error::RuntimeStartFailed)?; - - Ok(path_mapping) - } - - /// Sanitize a file path to be a valid ConfigMap key - /// Replaces '/' with '__' to comply with K8s key restrictions: [-._a-zA-Z0-9]+ - fn sanitize_configmap_key(path: &str) -> String { - path.replace('/', "__") - } - - /// Recursively walk directory and collect all files - async fn walk_directory( - current_path: &std::path::Path, - base_path: &std::path::Path, - text_data: &mut BTreeMap, - binary_data: &mut BTreeMap, - path_mapping: &mut BTreeMap, - ) -> Result<(), Error> { - use tokio::fs; - - let mut entries = fs::read_dir(current_path) - .await - .map_err(|_| Error::RuntimeStartFailed)?; - - while let Some(entry) = entries - .next_entry() - .await - .map_err(|_| Error::RuntimeStartFailed)? - { - let path = entry.path(); - - if path.is_dir() { - // Recursively process subdirectories - Box::pin(Self::walk_directory( - &path, - base_path, - text_data, - binary_data, - path_mapping, - )) - .await?; - } else if path.is_file() { - // Get relative path from base (e.g., "app/task.py") - let relative_path = path - .strip_prefix(base_path) - .map_err(|_| Error::RuntimeStartFailed)? - .to_str() - .ok_or(Error::RuntimeStartFailed)? - .to_string(); - - // Sanitize the key for ConfigMap (e.g., "app/task.py" -> "app__task.py") - let sanitized_key = Self::sanitize_configmap_key(&relative_path); - - // Store mapping for volume mount reconstruction - path_mapping.insert(sanitized_key.clone(), relative_path.clone()); - - // Try reading as text first - match fs::read_to_string(&path).await { - Ok(contents) => { - text_data.insert(sanitized_key, contents); - } - Err(_) => { - // If not text, read as binary - if let Ok(contents) = fs::read(&path).await { - binary_data.insert(sanitized_key, k8s_openapi::ByteString(contents)); - } - } - } - } - } - - Ok(()) - } - - /// Build service spec for networking - fn build_service_spec( - &self, - exec_id: &str, - networking: &NetworkingSpec, - ) -> Result { - let mut labels = BTreeMap::new(); - labels.insert("app".to_string(), "tower-app".to_string()); - labels.insert("execution-id".to_string(), exec_id.to_string()); - - let service_port = ServicePort { - name: Some("http".to_string()), - port: networking.port as i32, - target_port: Some( - k8s_openapi::apimachinery::pkg::util::intstr::IntOrString::Int( - networking.port as i32, - ), - ), - ..Default::default() - }; - - let service_spec = ServiceSpec { - selector: Some(labels.clone()), - ports: Some(vec![service_port]), - type_: Some("ClusterIP".to_string()), - ..Default::default() - }; - - Ok(Service { - metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta { - name: Some( - networking - .service_name - .clone() - .unwrap_or_else(|| format!("tower-svc-{}", exec_id)), - ), - namespace: Some(self.namespace.clone()), - labels: Some(labels), - ..Default::default() - }, - spec: Some(service_spec), - ..Default::default() - }) - } -} - -#[async_trait] -impl ExecutionBackend for K8sBackend { - type Handle = K8sHandle; - - async fn create(&self, spec: ExecutionSpec) -> Result { - let pods: Api = Api::namespaced(self.client.clone(), &self.namespace); - - // Create ConfigMap with bundle contents and get path mapping - let path_mapping = self.create_bundle_configmap(&spec).await?; - - // Build and create pod with path mapping for volume items - let pod = self.build_pod_spec(&spec, &path_mapping)?; - let pod_name = pod.metadata.name.clone().ok_or(Error::RuntimeStartFailed)?; - - pods.create(&PostParams::default(), &pod) - .await - .map_err(|_| Error::RuntimeStartFailed)?; - - // Create service if networking is specified - let service_endpoint = if let Some(networking) = &spec.networking { - if networking.expose_service { - let services: Api = Api::namespaced(self.client.clone(), &self.namespace); - let service = self.build_service_spec(&spec.id, networking)?; - let service_name = service - .metadata - .name - .clone() - .ok_or(Error::RuntimeStartFailed)?; - - services - .create(&PostParams::default(), &service) - .await - .map_err(|_| Error::RuntimeStartFailed)?; - - Some(ServiceEndpoint { - host: format!("{}.{}.svc.cluster.local", service_name, self.namespace), - port: networking.port, - protocol: "http".to_string(), - url: Some(format!( - "http://{}.{}.svc.cluster.local:{}", - service_name, self.namespace, networking.port - )), - }) - } else { - None - } - } else { - None - }; - - Ok(K8sHandle { - id: spec.id, - pod_name, - namespace: self.namespace.clone(), - client: self.client.clone(), - service_endpoint: Arc::new(Mutex::new(service_endpoint)), - }) - } - - fn capabilities(&self) -> BackendCapabilities { - BackendCapabilities { - name: "k8s".to_string(), - supports_persistent_cache: true, - supports_prewarming: true, - supports_network_isolation: true, - supports_service_endpoints: true, - typical_cold_start_ms: 5000, // ~5s for image pull + pod start - typical_warm_start_ms: 1000, // ~1s with cached image - max_concurrent_executions: None, // Limited by cluster capacity - } - } - - async fn cleanup(&self) -> Result<(), Error> { - // No global cleanup needed for K8s backend - Ok(()) - } -} - -/// K8sHandle provides lifecycle management for a Kubernetes Pod execution -pub struct K8sHandle { - id: String, - pod_name: String, - namespace: String, - client: Client, - service_endpoint: Arc>>, -} - -#[async_trait] -impl ExecutionHandle for K8sHandle { - fn id(&self) -> &str { - &self.id - } - - async fn status(&self) -> Result { - let pods: Api = Api::namespaced(self.client.clone(), &self.namespace); - - let pod = pods - .get(&self.pod_name) - .await - .map_err(|_| Error::NoRunningApp)?; - - let phase = pod - .status - .and_then(|s| s.phase) - .unwrap_or_else(|| "Unknown".to_string()); - - Ok(match phase.as_str() { - "Pending" => ExecutionStatus::Preparing, - "Running" => ExecutionStatus::Running, - "Succeeded" => ExecutionStatus::Succeeded, - "Failed" => ExecutionStatus::Failed { exit_code: 1 }, - _ => ExecutionStatus::Unknown, - }) - } - - async fn logs(&self) -> Result { - let pods: Api = Api::namespaced(self.client.clone(), &self.namespace); - let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); - - let pod_name = self.pod_name.clone(); - let pods_clone = pods.clone(); - - tokio::spawn(async move { - // Wait for pod to have containers created (Running, Succeeded, or Failed) - // This ensures we can stream logs even if the pod crashes - let condition = await_condition(pods_clone.clone(), &pod_name, |obj: Option<&Pod>| { - obj.and_then(|pod| pod.status.as_ref()) - .and_then(|status| status.phase.as_ref()) - .map(|phase| phase == "Running" || phase == "Succeeded" || phase == "Failed") - .unwrap_or(false) - }); - - // Wait with a timeout - if tokio::time::timeout(std::time::Duration::from_secs(60), condition) - .await - .is_ok() - { - let log_params = LogParams { - follow: true, - ..Default::default() - }; - - if let Ok(logs) = pods_clone.log_stream(&pod_name, &log_params).await { - // Convert futures AsyncBufRead to tokio AsyncRead - let compat_logs = logs.compat(); - let mut reader = BufReader::new(compat_logs).lines(); - while let Ok(Some(line)) = reader.next_line().await { - let log_line = LogLine { - timestamp: Utc::now(), - stream: LogStream::Stdout, // K8s combines stdout/stderr - channel: LogChannel::Program, - content: line, - }; - if tx.send(log_line).is_err() { - break; - } - } - } - } - }); - - Ok(rx) - } - - async fn terminate(&mut self) -> Result<(), Error> { - let pods: Api = Api::namespaced(self.client.clone(), &self.namespace); - - pods.delete(&self.pod_name, &DeleteParams::default()) - .await - .map_err(|_| Error::TerminateFailed)?; - - Ok(()) - } - - async fn kill(&mut self) -> Result<(), Error> { - // For K8s, kill is the same as terminate (pod deletion) - self.terminate().await - } - - async fn wait_for_completion(&self) -> Result { - let pods: Api = Api::namespaced(self.client.clone(), &self.namespace); - - // Wait for pod to reach terminal state - await_condition(pods.clone(), &self.pod_name, |obj: Option<&Pod>| { - obj.and_then(|pod| pod.status.as_ref()) - .and_then(|status| status.phase.as_ref()) - .map(|phase| phase == "Succeeded" || phase == "Failed") - .unwrap_or(false) - }) - .await - .map_err(|_| Error::Timeout)?; - - self.status().await - } - - async fn service_endpoint(&self) -> Result, Error> { - let endpoint = self.service_endpoint.lock().await; - Ok(endpoint.clone()) - } - - async fn cleanup(&mut self) -> Result<(), Error> { - // Delete pod - self.terminate().await?; - - // Delete ConfigMap with bundle - let configmaps: Api = Api::namespaced(self.client.clone(), &self.namespace); - let configmap_name = format!("bundle-{}", self.id); - let _ = configmaps - .delete(&configmap_name, &DeleteParams::default()) - .await; - - // Delete service if it exists - if let Some(endpoint) = self.service_endpoint.lock().await.as_ref() { - let services: Api = Api::namespaced(self.client.clone(), &self.namespace); - // Extract service name from hostname - let service_name = endpoint.host.split('.').next().unwrap_or("unknown"); - let _ = services - .delete(service_name, &DeleteParams::default()) - .await; - } - - Ok(()) - } -} diff --git a/crates/tower-runtime/src/lib.rs b/crates/tower-runtime/src/lib.rs index f32cb894..8d3bc41f 100644 --- a/crates/tower-runtime/src/lib.rs +++ b/crates/tower-runtime/src/lib.rs @@ -11,9 +11,6 @@ pub mod errors; pub mod execution; pub mod local; -#[cfg(feature = "k8s")] -pub mod k8s; - use errors::Error; #[derive(Copy, Clone)] From 164c3c89ff789c3c1c92df47d47a40deba934330 Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Mon, 12 Jan 2026 19:03:36 +0100 Subject: [PATCH 04/21] Minor --- Cargo.toml | 2 - crates/tower-runtime/src/execution.rs | 183 ++++++-------------------- 2 files changed, 38 insertions(+), 147 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 570803c0..af88974e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,8 +34,6 @@ futures-lite = "2.6" glob = "0.3" http = "1.1" indicatif = "0.17" -k8s-openapi = { version = "0.23", features = ["v1_31"] } -kube = { version = "0.95", features = ["runtime", "client", "derive"] } nix = { version = "0.30", features = ["signal"] } pem = "3" promptly = "0.3" diff --git a/crates/tower-runtime/src/execution.rs b/crates/tower-runtime/src/execution.rs index f7e02ee4..2771bc33 100644 --- a/crates/tower-runtime/src/execution.rs +++ b/crates/tower-runtime/src/execution.rs @@ -1,59 +1,8 @@ -//! Generic execution backend abstraction for Tower +//! Execution backend abstraction for Tower //! //! This module provides traits and types for abstracting execution backends, //! allowing Tower to support multiple compute substrates (local processes, -//! Kubernetes pods, microVMs, gVisor, etc.) through a uniform interface. -//! -//! # Key Design Principles -//! -//! - **No language/runtime assumptions** - Python, Node.js, etc. are backend details -//! - **Cache is container filesystem-based** - Image layers, bundle mounts, not language-specific -//! - **Backends handle runtime setup** - Dependency installation, environment prep -//! - **Lifecycle operations are backend-agnostic** - Start/stop/status work everywhere -//! -//! # Security Model for Shared Caches -//! -//! Tower uses a **tiered cache isolation strategy** to balance performance and security: -//! -//! ## Safe for Global Sharing (Read-Only) -//! -//! These caches are **content-addressable** and **cryptographically verified**, making them -//! safe to share across all tenants: -//! -//! - **Bundle cache**: Keyed by SHA256 checksum, always read-only -//! - **Container layer cache**: Keyed by digest, always read-only -//! -//! **Attack surface**: Minimal - content is verified before use, mounted read-only -//! -//! ## Require Isolation (Read-Write) -//! -//! These caches are **mutable** and **writable**, requiring isolation: -//! -//! - **Dependency caches** (pip, npm, cargo, etc.): Writable by apps during installation -//! -//! **Attack vectors if shared globally**: -//! - **Cache poisoning**: Malicious app writes bad dependencies to shared cache -//! - **Information disclosure**: App reads another tenant's private packages -//! - **Timing attacks**: Infer what dependencies other tenants use -//! -//! **Mitigation**: Use `CacheIsolation::PerAccount` or `CacheIsolation::PerApp` -//! -//! ## Recommended Configuration -//! -//! ```rust,ignore -//! // For Tower's multi-tenant SaaS: -//! CacheConfig { -//! enable_bundle_cache: true, -//! enable_runtime_cache: true, -//! enable_dependency_cache: true, -//! isolation: CacheIsolation::PerAccount { account_id: "acct-123" }, -//! } -//! ``` -//! -//! This gives: -//! - ✅ Bundle/layer sharing across all tenants (fast) -//! - ✅ Dependency cache sharing within an account (reasonable hit rate) -//! - ✅ Isolation between different accounts (secure) +//! Kubernetes pods, etc.) through a uniform interface. use async_trait::async_trait; use chrono::{DateTime, Utc}; @@ -165,27 +114,16 @@ pub struct CacheConfig { /// CacheIsolation defines security boundaries for caches #[derive(Debug, Clone)] pub enum CacheIsolation { - /// Global sharing (safe for content-addressable immutable content only) - /// - /// Use for: bundles (by checksum), container layers (by digest) - /// Security: Read-only mounts, content is cryptographically verified + /// Global sharing (safe for immutable content-addressable caches) Global, - /// Per-account isolation (share within account, isolate between accounts) - /// - /// Use for: dependency caches when you trust apps within same account - /// Security: Apps in account A cannot access account B's cache + /// Per-account isolation PerAccount { account_id: String }, - /// Per-app isolation (completely isolated, no sharing) - /// - /// Use for: maximum security, mutable caches in multi-tenant environments - /// Security: Each app has its own cache namespace + /// Per-app isolation PerApp { app_id: String }, - /// No isolation (single-tenant environments only) - /// - /// WARNING: Only use in trusted single-tenant environments + /// No isolation None, } @@ -217,14 +155,13 @@ pub struct ResourceLimits { /// Ephemeral storage limit in megabytes pub storage_mb: Option, - /// Maximum number of processes (prevents fork bombs) + /// Maximum number of processes pub max_pids: Option, - /// GPU count (0 = no GPU) + /// GPU count pub gpu_count: u32, - /// Execution timeout in seconds (0 = no timeout) - /// Control plane sets policy, runner may apply local ceiling + /// Execution timeout in seconds pub timeout_seconds: u32, } @@ -245,51 +182,45 @@ pub struct NetworkingSpec { // Execution Backend Trait // ============================================================================ -/// ExecutionBackend abstracts the compute substrate (K8s, microVM, gVisor, etc.) +/// ExecutionBackend abstracts the compute substrate #[async_trait] pub trait ExecutionBackend: Send + Sync { /// The handle type this backend returns type Handle: ExecutionHandle; /// Create a new execution environment - /// - /// This method is responsible for: - /// - Acquiring/downloading the bundle - /// - Setting up the runtime environment (using cache if available) - /// - Starting the application - /// - Returning a handle for lifecycle management async fn create(&self, spec: ExecutionSpec) -> Result; - /// Get backend capabilities (for optimization hints) + /// Get backend capabilities fn capabilities(&self) -> BackendCapabilities; - /// Cleanup backend resources (called on shutdown) + /// Cleanup backend resources async fn cleanup(&self) -> Result<(), Error>; } /// BackendCapabilities describes what a backend supports #[derive(Debug, Clone)] pub struct BackendCapabilities { - /// Backend name (for debugging/logging) + /// Backend name pub name: String, - /// Does backend support persistent volumes for caching? + /// Supports persistent volumes for caching pub supports_persistent_cache: bool, - /// Does backend support pre-warmed environments? + /// Supports pre-warmed environments pub supports_prewarming: bool, - /// Does backend support network isolation? + /// Supports network isolation pub supports_network_isolation: bool, - /// Does backend support service endpoints? + /// Supports service endpoints pub supports_service_endpoints: bool, - /// Typical startup latency characteristics (milliseconds) + /// Typical startup latency in milliseconds pub typical_cold_start_ms: u64, pub typical_warm_start_ms: u64, - /// Maximum concurrent executions this backend can handle + /// Maximum concurrent executions pub max_concurrent_executions: Option, } @@ -297,7 +228,7 @@ pub struct BackendCapabilities { // Execution Handle Trait // ============================================================================ -/// ExecutionHandle represents a running execution (Pod, microVM, process, etc.) +/// ExecutionHandle represents a running execution #[async_trait] pub trait ExecutionHandle: Send + Sync { /// Get unique identifier for this execution @@ -306,30 +237,22 @@ pub trait ExecutionHandle: Send + Sync { /// Get current execution status async fn status(&self) -> Result; - /// Subscribe to log stream (returns receiver for log lines) - /// - /// Note: Multiple calls may return the same stream or separate streams - /// depending on backend implementation + /// Subscribe to log stream async fn logs(&self) -> Result; - /// Terminate execution gracefully (SIGTERM equivalent) - /// - /// Returns when termination is initiated, not necessarily complete. - /// Use `wait_for_completion()` to wait for actual termination. + /// Terminate execution gracefully async fn terminate(&mut self) -> Result<(), Error>; - /// Force kill execution (SIGKILL equivalent) + /// Force kill execution async fn kill(&mut self) -> Result<(), Error>; - /// Wait for execution to complete (blocking) + /// Wait for execution to complete async fn wait_for_completion(&self) -> Result; - /// Get service endpoint (if this execution exposes a service) + /// Get service endpoint async fn service_endpoint(&self) -> Result, Error>; - /// Cleanup resources (delete pod, cleanup filesystem, etc.) - /// - /// Should be called after execution completes to free resources. + /// Cleanup resources async fn cleanup(&mut self) -> Result<(), Error>; } @@ -415,21 +338,16 @@ pub enum LogChannel { } // ============================================================================ -// App Trait Integration (High-Level Interface) +// App Trait Integration // ============================================================================ /// App trait provides high-level lifecycle management -/// -/// This trait is the user-facing interface used by AppLauncher and CLI code. -/// Implementations use ExecutionBackend internally to provide isolation. #[async_trait] pub trait App: Send + Sync { - /// The backend type this App uses for execution + /// The backend type this App uses type Backend: ExecutionBackend; /// Start a new execution - /// - /// The backend is passed explicitly to avoid static method limitations. async fn start(backend: Arc, opts: StartOptions) -> Result where Self: Sized; @@ -437,18 +355,16 @@ pub trait App: Send + Sync { /// Get current execution status async fn status(&self) -> Result; - /// Terminate execution gracefully + /// Terminate execution async fn terminate(&mut self) -> Result<(), Error>; - /// Get service endpoint (if applicable) + /// Get service endpoint async fn service_endpoint(&self) -> Result, Error> { Ok(None) } } /// StartOptions contains all parameters needed to start an execution -/// -/// This is the same structure used by existing CLI/runner code, kept for compatibility. pub struct StartOptions { pub ctx: tower_telemetry::Context, pub package: tower_package::Package, @@ -462,8 +378,6 @@ pub struct StartOptions { } /// AppLauncher orchestrates App lifecycle -/// -/// Generic over App type, which determines the backend via associated type. pub struct AppLauncher { backend: Arc, app: Option, @@ -514,9 +428,7 @@ impl AppLauncher { } } -/// ManagedApp is the primary implementation of App that uses ExecutionBackend -/// -/// This is the bridge between the high-level App trait and low-level ExecutionBackend. +/// ManagedApp implements App using ExecutionBackend pub struct ManagedApp { backend: Arc, handle: Option, @@ -648,10 +560,7 @@ fn convert_start_options_to_spec(opts: StartOptions) -> Result Result, Error>; /// Store bundle in cache - /// - /// Bundles are stored globally with CacheIsolation::Global async fn put_bundle( &self, bundle_id: &str, @@ -668,10 +575,7 @@ pub trait CacheManager: Send + Sync { source: CacheSource, ) -> Result; - /// Get cached runtime layer, return handle if hit - /// - /// Runtime layers are content-addressable by digest, safe for global sharing. - /// Always returns read-only handle. + /// Get cached runtime layer async fn get_runtime_layer( &self, image: &str, @@ -679,8 +583,6 @@ pub trait CacheManager: Send + Sync { ) -> Result, Error>; /// Store runtime layer in cache - /// - /// Layers are stored globally with CacheIsolation::Global async fn put_runtime_layer( &self, image: &str, @@ -688,22 +590,14 @@ pub trait CacheManager: Send + Sync { source: CacheSource, ) -> Result; - /// Get cache directory for language-specific dependencies - /// - /// Returns a handle to a cache directory that can be mounted into - /// the execution environment (e.g., for pip cache, npm cache, etc.) - /// - /// SECURITY: This returns a read-write handle isolated by the provided isolation strategy. - /// - CacheIsolation::Global: NOT RECOMMENDED (writable by all apps) - /// - CacheIsolation::PerAccount: Shared within account (recommended for Tower) - /// - CacheIsolation::PerApp: Fully isolated per app (maximum security) + /// Get cache directory for dependencies async fn get_dependency_cache( &self, language: &str, isolation: CacheIsolation, ) -> Result; - /// Cleanup old cache entries (LRU eviction, TTL expiration, etc.) + /// Cleanup old cache entries async fn cleanup(&self, max_age_seconds: u64) -> Result; } @@ -746,10 +640,10 @@ pub enum CacheLocation { /// CachePermissions defines what operations are allowed on cached data #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CachePermissions { - /// Read-only access (for immutable content like bundles, layers) + /// Read-only access ReadOnly, - /// Read-write access (for mutable caches like dependency caches) + /// Read-write access ReadWrite, } @@ -777,5 +671,4 @@ pub struct CleanupStats { // Concrete Backend Implementations // ============================================================================ -// LocalBackend and K8sBackend will be implemented in separate modules -// See local.rs and k8s.rs (feature-gated) +// LocalBackend implemented in local.rs From c066ab6cb1e40eb3ba150ae37ef45d6e0654fca3 Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Mon, 12 Jan 2026 19:12:12 +0100 Subject: [PATCH 05/21] Remove cache abstractions --- crates/tower-runtime/src/execution.rs | 262 +------------------------- crates/tower-runtime/src/local.rs | 1 - 2 files changed, 1 insertion(+), 262 deletions(-) diff --git a/crates/tower-runtime/src/execution.rs b/crates/tower-runtime/src/execution.rs index 2771bc33..b774ba57 100644 --- a/crates/tower-runtime/src/execution.rs +++ b/crates/tower-runtime/src/execution.rs @@ -54,23 +54,8 @@ pub struct ExecutionSpec { /// BundleRef describes where to get the application bundle #[derive(Debug, Clone)] pub enum BundleRef { - /// Local filesystem path (for local execution) + /// Local filesystem path Local { path: PathBuf }, - - /// Remote bundle to be downloaded - Remote { - bundle_id: String, - checksum: String, - download_url: String, - }, - - /// Container image reference - ContainerImage { - registry: String, - repository: String, - tag: String, - digest: Option, - }, } /// RuntimeConfig specifies the execution runtime environment @@ -133,12 +118,6 @@ pub enum CacheBackend { /// Local filesystem cache Local { cache_dir: PathBuf }, - /// Kubernetes PersistentVolume - K8sPersistentVolume { pv_claim_name: String }, - - /// S3-based cache - S3 { bucket: String, prefix: String }, - /// No caching None, } @@ -428,245 +407,6 @@ impl AppLauncher { } } -/// ManagedApp implements App using ExecutionBackend -pub struct ManagedApp { - backend: Arc, - handle: Option, - output_forwarder: Option>, -} - -#[async_trait] -impl App for ManagedApp { - type Backend = B; - - async fn start(backend: Arc, opts: StartOptions) -> Result { - // Extract output_sender before consuming opts - let output_sender = opts.output_sender.clone(); - - // Convert StartOptions to ExecutionSpec (consumes opts) - let spec = convert_start_options_to_spec(opts)?; - - // Create execution using backend - let handle = backend.create(spec).await?; - - // Start log forwarding - let mut log_receiver = handle.logs().await?; - let output_forwarder = tokio::spawn(async move { - while let Some(log_line) = log_receiver.recv().await { - // Convert LogLine to Output for existing code - let _ = output_sender.send(crate::Output { - channel: match log_line.channel { - LogChannel::Setup => crate::Channel::Setup, - LogChannel::Program => crate::Channel::Program, - }, - time: log_line.timestamp, - fd: match log_line.stream { - LogStream::Stdout => crate::FD::Stdout, - LogStream::Stderr => crate::FD::Stderr, - }, - line: log_line.content, - }); - } - }); - - Ok(Self { - backend, - handle: Some(handle), - output_forwarder: Some(output_forwarder), - }) - } - - async fn status(&self) -> Result { - self.handle.as_ref().ok_or(Error::NoHandle)?.status().await - } - - async fn terminate(&mut self) -> Result<(), Error> { - if let Some(mut handle) = self.handle.take() { - handle.terminate().await?; - handle.cleanup().await?; - } - - // Stop log forwarding - if let Some(forwarder) = self.output_forwarder.take() { - forwarder.abort(); - } - - Ok(()) - } - - async fn service_endpoint(&self) -> Result, Error> { - match &self.handle { - Some(handle) => handle.service_endpoint().await, - None => Ok(None), - } - } -} - -/// Helper function to convert StartOptions to ExecutionSpec -fn convert_start_options_to_spec(opts: StartOptions) -> Result { - let package = opts.package; - - // Determine bundle reference from package - let bundle = if let Some(path) = package.unpacked_path { - BundleRef::Local { path } - } else { - // TODO: Handle remote bundles - return Err(Error::InvalidPackage); - }; - - // Build runtime config from cache_dir - let runtime = RuntimeConfig { - image: "towerhq/tower-runtime:latest".to_string(), // TODO: Make configurable - version: None, - cache: CacheConfig { - enable_bundle_cache: true, - enable_runtime_cache: true, - enable_dependency_cache: true, - backend: match opts.cache_dir { - Some(dir) => CacheBackend::Local { cache_dir: dir }, - None => CacheBackend::None, - }, - isolation: CacheIsolation::None, // TODO: Get from context - }, - entrypoint: None, - command: None, - }; - - Ok(ExecutionSpec { - id: uuid::Uuid::new_v4().to_string(), // TODO: Use actual run_id - bundle, - runtime, - environment: opts.environment, - secrets: opts.secrets, - parameters: opts.parameters, - env_vars: opts.env_vars, - resources: ResourceLimits { - cpu_millicores: None, - memory_mb: None, - storage_mb: None, - max_pids: None, - gpu_count: 0, - timeout_seconds: 0, // No timeout by default - }, - networking: None, - telemetry_ctx: opts.ctx, - }) -} - -// ============================================================================ -// Cache Manager Trait -// ============================================================================ - -/// CacheManager abstracts caching across execution backends -#[async_trait] -pub trait CacheManager: Send + Sync { - /// Get cached bundle - async fn get_bundle( - &self, - bundle_id: &str, - checksum: &str, - ) -> Result, Error>; - - /// Store bundle in cache - async fn put_bundle( - &self, - bundle_id: &str, - checksum: &str, - source: CacheSource, - ) -> Result; - - /// Get cached runtime layer - async fn get_runtime_layer( - &self, - image: &str, - layer_digest: &str, - ) -> Result, Error>; - - /// Store runtime layer in cache - async fn put_runtime_layer( - &self, - image: &str, - layer_digest: &str, - source: CacheSource, - ) -> Result; - - /// Get cache directory for dependencies - async fn get_dependency_cache( - &self, - language: &str, - isolation: CacheIsolation, - ) -> Result; - - /// Cleanup old cache entries - async fn cleanup(&self, max_age_seconds: u64) -> Result; -} - -/// CacheHandle represents a cached item that can be used by a backend -#[derive(Debug, Clone)] -pub struct CacheHandle { - /// The actual storage location - pub location: CacheLocation, - - /// Access permissions for this cache - pub permissions: CachePermissions, - - /// Isolation context (which account/app can access this) - pub isolation: CacheIsolation, -} - -/// CacheLocation describes where the cached data lives -#[derive(Debug, Clone)] -pub enum CacheLocation { - /// Local filesystem path - Local(PathBuf), - - /// Kubernetes PersistentVolume mount specification - K8sVolume { - pv_claim_name: String, - subpath: String, - }, - - /// S3 location - S3 { - bucket: String, - key: String, - etag: Option, - }, - - /// Container image layer (already in container runtime) - ContainerLayer { layer_id: String }, -} - -/// CachePermissions defines what operations are allowed on cached data -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CachePermissions { - /// Read-only access - ReadOnly, - - /// Read-write access - ReadWrite, -} - -/// CacheSource describes where to get data for caching -#[derive(Debug)] -pub enum CacheSource { - /// Copy from local filesystem - LocalPath(PathBuf), - - /// Download from URL - Url(String), - - /// Raw bytes - Bytes(Vec), -} - -/// CleanupStats reports cache cleanup results -#[derive(Debug, Clone)] -pub struct CleanupStats { - pub entries_removed: u32, - pub bytes_freed: u64, -} - // ============================================================================ // Concrete Backend Implementations // ============================================================================ diff --git a/crates/tower-runtime/src/local.rs b/crates/tower-runtime/src/local.rs index 2045a54f..07c71d05 100644 --- a/crates/tower-runtime/src/local.rs +++ b/crates/tower-runtime/src/local.rs @@ -614,7 +614,6 @@ impl ExecutionBackend for LocalBackend { ctx: spec.telemetry_ctx, package: match spec.bundle { BundleRef::Local { path } => Package::from_unpacked_path(path).await, - _ => return Err(Error::NotImplemented), // Remote bundles not yet supported }, cwd: None, // LocalApp determines cwd from package environment: spec.environment, From 3fb7831a6e62f78201ba97c198142b5c08470e6c Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Tue, 13 Jan 2026 12:42:57 +0100 Subject: [PATCH 06/21] Refactor run.rs correctly --- crates/config/src/session.rs | 4 +- crates/tower-cmd/src/run.rs | 41 ++++--- crates/tower-runtime/src/execution.rs | 170 +------------------------- crates/tower-runtime/src/lib.rs | 98 ++++++++++----- crates/tower-runtime/src/local.rs | 45 ++----- 5 files changed, 111 insertions(+), 247 deletions(-) diff --git a/crates/config/src/session.rs b/crates/config/src/session.rs index 53830ccf..f7a22b07 100644 --- a/crates/config/src/session.rs +++ b/crates/config/src/session.rs @@ -21,7 +21,9 @@ fn extract_aid_from_jwt(jwt: &str) -> Option { let payload = parts[1]; let decoded = URL_SAFE_NO_PAD.decode(payload).ok()?; let json: serde_json::Value = serde_json::from_slice(&decoded).ok()?; - json.get("https://tower.dev/aid")?.as_str().map(String::from) + json.get("https://tower.dev/aid")? + .as_str() + .map(String::from) } const DEFAULT_TOWER_URL: &str = "https://api.tower.dev"; diff --git a/crates/tower-cmd/src/run.rs b/crates/tower-cmd/src/run.rs index 50a3de07..92bab124 100644 --- a/crates/tower-cmd/src/run.rs +++ b/crates/tower-cmd/src/run.rs @@ -5,12 +5,14 @@ use std::collections::HashMap; use std::path::PathBuf; use tower_api::models::Run; use tower_package::{Package, PackageSpec}; -use tower_runtime::{local::LocalApp, App, AppLauncher, OutputReceiver, Status}; +use tower_runtime::{ + execution::ExecutionHandle, local::LocalApp, AppLauncher, OutputReceiver, Status, +}; use tower_telemetry::{debug, Context}; use std::sync::Arc; use tokio::sync::{ - mpsc::{unbounded_channel, Receiver as MpscReceiver}, + mpsc::Receiver as MpscReceiver, oneshot::{self, Receiver as OneshotReceiver}, Mutex, }; @@ -168,28 +170,32 @@ where // Unpack the package package.unpack().await?; - let (sender, receiver) = unbounded_channel(); - output::success(&format!("Launching app `{}`", towerfile.app.name)); - let output_task = tokio::spawn(output_handler(receiver)); + // Create backend and launcher + use tower_runtime::local::LocalBackend; + let backend = LocalBackend::new(config.cache_dir); let mut launcher: AppLauncher = AppLauncher::default(); + launcher .launch( + backend, Context::new(), - sender, package, env.to_string(), secrets, params, env_vars, - config.cache_dir, ) .await?; - // Monitor app output and status concurrently - let app = Arc::new(Mutex::new(launcher.app.unwrap())); - let status_task = tokio::spawn(monitor_local_status(Arc::clone(&app))); + // Get logs from handle and spawn output handler + let logs_receiver = launcher.handle.as_ref().unwrap().logs().await?; + let output_task = tokio::spawn(output_handler(logs_receiver)); + + // Monitor app status concurrently + let handle = Arc::new(Mutex::new(launcher.handle.take().unwrap())); + let status_task = tokio::spawn(monitor_handle_status(Arc::clone(&handle))); // Wait for app to complete or SIGTERM let status_result = tokio::select! { @@ -199,7 +205,7 @@ where }, _ = tokio::signal::ctrl_c(), if !output::get_output_mode().is_mcp() => { output::write("\nReceived Ctrl+C, stopping local run...\n"); - app.lock().await.terminate().await.ok(); + handle.lock().await.terminate().await.ok(); return Ok(output_task.await.unwrap()); } }; @@ -595,8 +601,8 @@ async fn monitor_output(mut output: OutputReceiver) { /// monitor_local_status is a helper function that will monitor the status of a given app and waits for /// it to progress to a terminal state. -async fn monitor_local_status(app: Arc>) -> Status { - debug!("Starting status monitoring for LocalApp"); +async fn monitor_handle_status(handle: Arc>) -> Status { + debug!("Starting status monitoring for execution handle"); let mut check_count = 0; let mut err_count = 0; @@ -604,11 +610,11 @@ async fn monitor_local_status(app: Arc>) -> Status { check_count += 1; debug!( - "Status check #{}, attempting to get app status", + "Status check #{}, attempting to get handle status", check_count ); - match app.lock().await.status().await { + match tower_runtime::execution::ExecutionHandle::status(&*handle.lock().await).await { Ok(status) => { // We reset the error count to indicate that we can intermittently get statuses. err_count = 0; @@ -627,17 +633,18 @@ async fn monitor_local_status(app: Arc>) -> Status { return status; } _ => { + debug!("Handle status: other, continuing to monitor"); sleep(Duration::from_millis(100)).await; } } } Err(e) => { - debug!("Failed to get app status: {:?}", e); + debug!("Failed to get handle status: {:?}", e); err_count += 1; // If we get five errors in a row, we abandon monitoring. if err_count >= 5 { - debug!("Failed to get app status after 5 attempts, giving up"); + debug!("Failed to get handle status after 5 attempts, giving up"); output::error("An error occured while monitoring your local run status!"); return tower_runtime::Status::Crashed { code: -1 }; } diff --git a/crates/tower-runtime/src/execution.rs b/crates/tower-runtime/src/execution.rs index b774ba57..b84ed7fd 100644 --- a/crates/tower-runtime/src/execution.rs +++ b/crates/tower-runtime/src/execution.rs @@ -5,13 +5,11 @@ //! Kubernetes pods, etc.) through a uniform interface. use async_trait::async_trait; -use chrono::{DateTime, Utc}; use std::collections::HashMap; use std::path::PathBuf; -use std::sync::Arc; -use tokio::sync::mpsc::UnboundedReceiver; use crate::errors::Error; +use crate::{OutputReceiver, Status}; // ============================================================================ // Core Execution Types @@ -214,10 +212,10 @@ pub trait ExecutionHandle: Send + Sync { fn id(&self) -> &str; /// Get current execution status - async fn status(&self) -> Result; + async fn status(&self) -> Result; /// Subscribe to log stream - async fn logs(&self) -> Result; + async fn logs(&self) -> Result; /// Terminate execution gracefully async fn terminate(&mut self) -> Result<(), Error>; @@ -226,7 +224,7 @@ pub trait ExecutionHandle: Send + Sync { async fn kill(&mut self) -> Result<(), Error>; /// Wait for execution to complete - async fn wait_for_completion(&self) -> Result; + async fn wait_for_completion(&self) -> Result; /// Get service endpoint async fn service_endpoint(&self) -> Result, Error>; @@ -235,31 +233,6 @@ pub trait ExecutionHandle: Send + Sync { async fn cleanup(&mut self) -> Result<(), Error>; } -/// ExecutionStatus represents the current state of an execution -#[derive(Debug, Clone, PartialEq)] -pub enum ExecutionStatus { - /// Execution is being prepared (downloading bundle, setting up environment) - Preparing, - - /// Execution is currently running - Running, - - /// Execution completed successfully (exit code 0) - Succeeded, - - /// Execution failed with non-zero exit code - Failed { exit_code: i32 }, - - /// Execution crashed (segfault, OOM kill, etc.) - Crashed { reason: String }, - - /// Execution was terminated by user/system - Terminated, - - /// Unknown status (shouldn't happen in normal operation) - Unknown, -} - /// ServiceEndpoint describes how to reach a running service #[derive(Debug, Clone)] pub struct ServiceEndpoint { @@ -276,139 +249,4 @@ pub struct ServiceEndpoint { pub url: Option, } -// ============================================================================ -// Log Streaming Types -// ============================================================================ - -/// LogReceiver is a stream of log lines from the execution -pub type LogReceiver = UnboundedReceiver; - -/// LogLine represents a single line of output -#[derive(Debug, Clone)] -pub struct LogLine { - /// When this line was emitted - pub timestamp: DateTime, - - /// Which stream (stdout/stderr) - pub stream: LogStream, - - /// Which phase (setup vs program) - pub channel: LogChannel, - - /// The actual log content - pub content: String, -} - -/// LogStream identifies stdout vs stderr -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum LogStream { - Stdout, - Stderr, -} - -/// LogChannel identifies setup vs program output -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum LogChannel { - /// Setup phase (dependency installation, environment prep) - Setup, - - /// Program phase (actual application output) - Program, -} - -// ============================================================================ -// App Trait Integration -// ============================================================================ - -/// App trait provides high-level lifecycle management -#[async_trait] -pub trait App: Send + Sync { - /// The backend type this App uses - type Backend: ExecutionBackend; - - /// Start a new execution - async fn start(backend: Arc, opts: StartOptions) -> Result - where - Self: Sized; - - /// Get current execution status - async fn status(&self) -> Result; - - /// Terminate execution - async fn terminate(&mut self) -> Result<(), Error>; - - /// Get service endpoint - async fn service_endpoint(&self) -> Result, Error> { - Ok(None) - } -} - -/// StartOptions contains all parameters needed to start an execution -pub struct StartOptions { - pub ctx: tower_telemetry::Context, - pub package: tower_package::Package, - pub cwd: Option, - pub environment: String, - pub secrets: HashMap, - pub parameters: HashMap, - pub env_vars: HashMap, - pub output_sender: tokio::sync::mpsc::UnboundedSender, - pub cache_dir: Option, -} - -/// AppLauncher orchestrates App lifecycle -pub struct AppLauncher { - backend: Arc, - app: Option, -} - -impl AppLauncher { - /// Create a new launcher with the specified backend - pub fn new(backend: Arc) -> Self { - Self { backend, app: None } - } - - /// Launch an app with the given options - pub async fn launch(&mut self, opts: StartOptions) -> Result<(), Error> { - // Drop any existing app - self.app = None; - - // Start new app using backend - let app = A::start(self.backend.clone(), opts).await?; - self.app = Some(app); - - Ok(()) - } - - /// Get current app status - pub async fn status(&self) -> Result { - self.app - .as_ref() - .ok_or(Error::AppNotStarted)? - .status() - .await - } - - /// Terminate the running app - pub async fn terminate(&mut self) -> Result<(), Error> { - if let Some(app) = &mut self.app { - app.terminate().await?; - self.app = None; - } - Ok(()) - } - - /// Get service endpoint (if app exposes one) - pub async fn service_endpoint(&self) -> Result, Error> { - match &self.app { - Some(app) => app.service_endpoint().await, - None => Ok(None), - } - } -} - -// ============================================================================ -// Concrete Backend Implementations -// ============================================================================ - // LocalBackend implemented in local.rs diff --git a/crates/tower-runtime/src/lib.rs b/crates/tower-runtime/src/lib.rs index 8d3bc41f..162910c6 100644 --- a/crates/tower-runtime/src/lib.rs +++ b/crates/tower-runtime/src/lib.rs @@ -12,6 +12,7 @@ pub mod execution; pub mod local; use errors::Error; +use execution::{ExecutionBackend, ExecutionHandle}; #[derive(Copy, Clone)] pub enum FD { @@ -48,7 +49,9 @@ pub type OutputReceiver = UnboundedReceiver; pub type OutputSender = UnboundedSender; -pub trait App { +pub trait App: Send + Sync { + type Backend: execution::ExecutionBackend; + // start will start the process fn start(opts: StartOptions) -> impl Future> + Send where @@ -62,70 +65,107 @@ pub trait App { } pub struct AppLauncher { - pub app: Option, + pub handle: Option<::Handle>, } impl std::default::Default for AppLauncher { fn default() -> Self { - Self { app: None } + Self { handle: None } } } -impl AppLauncher { +impl AppLauncher +where + A::Backend: execution::ExecutionBackend, +{ pub async fn launch( &mut self, + backend: A::Backend, ctx: tower_telemetry::Context, - output_sender: OutputSender, package: Package, environment: String, secrets: HashMap, parameters: HashMap, env_vars: HashMap, - cache_dir: Option, ) -> Result<(), Error> { - let cwd = package.unpacked_path.clone().unwrap().to_path_buf(); - - let opts = StartOptions { - ctx, - output_sender, - cwd: Some(cwd), + let package_path = package.unpacked_path.clone().unwrap().to_path_buf(); + + // Build ExecutionSpec from parameters + use std::time::{SystemTime, UNIX_EPOCH}; + let id = format!( + "run-{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + ); + + let spec = execution::ExecutionSpec { + id, + bundle: execution::BundleRef::Local { + path: package_path.clone(), + }, + runtime: execution::RuntimeConfig { + image: "local".to_string(), + version: None, + cache: execution::CacheConfig { + enable_bundle_cache: true, + enable_runtime_cache: true, + enable_dependency_cache: true, + backend: execution::CacheBackend::None, + isolation: execution::CacheIsolation::None, + }, + entrypoint: None, + command: None, + }, environment, secrets, parameters, - package, env_vars, - cache_dir, + resources: execution::ResourceLimits { + cpu_millicores: None, + memory_mb: None, + storage_mb: None, + max_pids: None, + gpu_count: 0, + timeout_seconds: 3600, + }, + networking: None, + telemetry_ctx: ctx, }; - // NOTE: This is a really awful hack to force any existing app to drop itself. Not certain - // this is exactly what we want to do... - self.app = None; + // Drop any existing handle + self.handle = None; - let res = A::start(opts).await; + // Create execution using backend + let handle = backend.create(spec).await?; + self.handle = Some(handle); - if let Ok(app) = res { - self.app = Some(app); - Ok(()) - } else { - self.app = None; - Err(res.err().unwrap()) - } + Ok(()) } pub async fn terminate(&mut self) -> Result<(), Error> { - if let Some(app) = &mut self.app { - if let Err(err) = app.terminate().await { + if let Some(handle) = &mut self.handle { + if let Err(err) = handle.terminate().await { debug!("failed to terminate app: {}", err); Err(err) } else { - self.app = None; + self.handle = None; Ok(()) } } else { - // There's no app, so nothing to terminate. + // There's no handle, so nothing to terminate. Ok(()) } } + + pub async fn status(&self) -> Result { + if let Some(handle) = &self.handle { + handle.status().await + } else { + Ok(Status::None) + } + } } pub struct StartOptions { diff --git a/crates/tower-runtime/src/local.rs b/crates/tower-runtime/src/local.rs index 07c71d05..e003a462 100644 --- a/crates/tower-runtime/src/local.rs +++ b/crates/tower-runtime/src/local.rs @@ -6,7 +6,7 @@ use std::process::Stdio; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; -use crate::{errors::Error, OutputSender, StartOptions, Status}; +use crate::{errors::Error, OutputReceiver, OutputSender, StartOptions, Status}; use tokio::{ fs, @@ -324,6 +324,8 @@ impl Drop for LocalApp { } impl App for LocalApp { + type Backend = LocalBackend; + async fn start(opts: StartOptions) -> Result { let terminator = CancellationToken::new(); @@ -579,7 +581,7 @@ fn is_bash_package(package: &Package) -> bool { use crate::execution::{ BackendCapabilities, BundleRef, CacheBackend, ExecutionBackend, ExecutionHandle, ExecutionSpec, - ExecutionStatus, LogChannel, LogLine, LogReceiver, LogStream, ServiceEndpoint, + ServiceEndpoint, }; use async_trait::async_trait; use std::sync::Arc; @@ -666,46 +668,21 @@ impl ExecutionHandle for LocalHandle { &self.id } - async fn status(&self) -> Result { + async fn status(&self) -> Result { let app = self.app.lock().await; - let status = app.status().await?; - - Ok(match status { - Status::None => ExecutionStatus::Preparing, - Status::Running => ExecutionStatus::Running, - Status::Exited => ExecutionStatus::Succeeded, - Status::Crashed { code } => { - if code == -1 { - ExecutionStatus::Terminated - } else { - ExecutionStatus::Failed { exit_code: code } - } - } - }) + app.status().await } - async fn logs(&self) -> Result { + async fn logs(&self) -> Result { // Create a new channel for log streaming let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); - // Spawn a task to convert Output to LogLine + // Spawn a task to forward Output from the internal receiver let output_receiver = self.output_receiver.clone(); tokio::spawn(async move { let mut receiver = output_receiver.lock().await; while let Some(output) = receiver.recv().await { - let log_line = LogLine { - timestamp: output.time, - stream: match output.fd { - FD::Stdout => LogStream::Stdout, - FD::Stderr => LogStream::Stderr, - }, - channel: match output.channel { - Channel::Setup => LogChannel::Setup, - Channel::Program => LogChannel::Program, - }, - content: output.line, - }; - if tx.send(log_line).is_err() { + if tx.send(output).is_err() { break; // Receiver dropped } } @@ -724,11 +701,11 @@ impl ExecutionHandle for LocalHandle { self.terminate().await } - async fn wait_for_completion(&self) -> Result { + async fn wait_for_completion(&self) -> Result { loop { let status = self.status().await?; match status { - ExecutionStatus::Preparing | ExecutionStatus::Running => { + Status::None | Status::Running => { tokio::time::sleep(Duration::from_millis(100)).await; } _ => return Ok(status), From 03c9a6d895ae85729662595870f6a4d8166cf678 Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Tue, 13 Jan 2026 17:11:40 +0100 Subject: [PATCH 07/21] Refactor back the k8s runtime --- Cargo.lock | 628 ++++++++++++++++++++- crates/tower-cmd/src/run.rs | 6 +- crates/tower-runtime/Cargo.toml | 8 + crates/tower-runtime/src/backends/k8s.rs | 607 ++++++++++++++++++++ crates/tower-runtime/src/backends/local.rs | 155 +++++ crates/tower-runtime/src/backends/mod.rs | 6 + crates/tower-runtime/src/lib.rs | 1 + crates/tower-runtime/src/local.rs | 154 +---- 8 files changed, 1405 insertions(+), 160 deletions(-) create mode 100644 crates/tower-runtime/src/backends/k8s.rs create mode 100644 crates/tower-runtime/src/backends/local.rs create mode 100644 crates/tower-runtime/src/backends/mod.rs diff --git a/Cargo.lock b/Cargo.lock index a69ea5b6..365da75c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,19 @@ dependencies = [ "subtle", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.3", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -61,6 +74,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -132,6 +151,18 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-compression" version = "0.4.27" @@ -254,6 +285,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "getrandom 0.2.16", + "instant", + "rand 0.8.5", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -375,7 +417,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -489,6 +531,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "config" version = "0.3.39" @@ -528,6 +579,16 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[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" @@ -904,6 +965,18 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "either" version = "1.15.0" @@ -922,6 +995,26 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -948,6 +1041,27 @@ dependencies = [ "str-buf", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "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 = "eventsource-stream" version = "0.2.3" @@ -998,6 +1112,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fluent-uri" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1227,12 +1350,46 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + [[package]] name = "hashbrown" version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.4.1" @@ -1251,6 +1408,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "http" version = "1.3.1" @@ -1323,6 +1489,26 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-http-proxy" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ad4b0a1e37510028bc4ba81d0e38d239c39671b0f0ce9e02dfa93a8133f7c08" +dependencies = [ + "bytes", + "futures-util", + "headers", + "http", + "hyper", + "hyper-rustls", + "hyper-util", + "pin-project-lite", + "rustls-native-certs 0.7.3", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-rustls" version = "0.27.7" @@ -1332,7 +1518,9 @@ dependencies = [ "http", "hyper", "hyper-util", + "log", "rustls", + "rustls-native-certs 0.8.3", "rustls-pki-types", "tokio", "tokio-rustls", @@ -1340,6 +1528,19 @@ dependencies = [ "webpki-roots", ] +[[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.16" @@ -1567,6 +1768,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "inventory" version = "0.3.21" @@ -1656,6 +1866,167 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json-patch" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b1fb8864823fad91877e6caea0baca82e49e8db50f8e5c9f9a453e27d3330fc" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonpath-rust" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d8fe85bd70ff715f31ce8c739194b423d79811a19602115d611a3ec85d6200" +dependencies = [ + "lazy_static", + "once_cell", + "pest", + "pest_derive", + "regex", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c6e529149475ca0b2820835d3dce8fcc41c6b943ca608d32f35b449255e4627" +dependencies = [ + "fluent-uri", + "serde", + "serde_json", +] + +[[package]] +name = "k8s-openapi" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8847402328d8301354c94d605481f25a6bdc1ed65471fd96af8eca71141b13" +dependencies = [ + "base64", + "chrono", + "serde", + "serde-value", + "serde_json", +] + +[[package]] +name = "kube" +version = "0.96.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efffeb3df0bd4ef3e5d65044573499c0e4889b988070b08c50b25b1329289a1f" +dependencies = [ + "k8s-openapi", + "kube-client", + "kube-core", + "kube-derive", + "kube-runtime", +] + +[[package]] +name = "kube-client" +version = "0.96.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf471ece8ff8d24735ce78dac4d091e9fcb8d74811aeb6b75de4d1c3f5de0f1" +dependencies = [ + "base64", + "bytes", + "chrono", + "either", + "futures", + "home", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-http-proxy", + "hyper-rustls", + "hyper-timeout", + "hyper-util", + "jsonpath-rust", + "k8s-openapi", + "kube-core", + "pem", + "rustls", + "rustls-pemfile", + "secrecy", + "serde", + "serde_json", + "serde_yaml", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "tower 0.5.2", + "tower-http", + "tracing", +] + +[[package]] +name = "kube-core" +version = "0.96.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42346d30bb34d1d7adc5c549b691bce7aa3a1e60254e68fab7e2d7b26fe3d77" +dependencies = [ + "chrono", + "form_urlencoded", + "http", + "json-patch", + "k8s-openapi", + "schemars 0.8.22", + "serde", + "serde-value", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "kube-derive" +version = "0.96.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9364e04cc5e0482136c6ee8b7fb7551812da25802249f35b3def7aaa31e82ad" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "serde_json", + "syn 2.0.104", +] + +[[package]] +name = "kube-runtime" +version = "0.96.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fbf1f6ffa98e65f1d2a9a69338bb60605d46be7edf00237784b89e62c9bd44" +dependencies = [ + "ahash", + "async-broadcast", + "async-stream", + "async-trait", + "backoff", + "educe", + "futures", + "hashbrown 0.14.5", + "json-patch", + "jsonptr", + "k8s-openapi", + "kube-client", + "parking_lot", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "lazy-regex" version = "3.4.1" @@ -2017,12 +2388,33 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-probe" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" + [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "overload" version = "0.1.1" @@ -2116,6 +2508,49 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "pest_meta" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +dependencies = [ + "pest", + "sha2", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -2668,6 +3103,7 @@ version = "0.23.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", @@ -2676,6 +3112,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe 0.1.6", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework 2.11.1", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe 0.2.0", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.12.0" @@ -2742,6 +3212,27 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive 0.8.22", + "serde", + "serde_json", +] + [[package]] name = "schemars" version = "0.9.0" @@ -2763,11 +3254,23 @@ dependencies = [ "chrono", "dyn-clone", "ref-cast", - "schemars_derive", + "schemars_derive 1.0.4", "serde", "serde_json", ] +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.104", +] + [[package]] name = "schemars_derive" version = "1.0.4" @@ -2798,6 +3301,51 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.219" @@ -2807,6 +3355,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.219" @@ -2915,6 +3473,30 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.10.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3450,6 +4032,7 @@ dependencies = [ "futures-io", "futures-sink", "pin-project-lite", + "slab", "tokio", ] @@ -3514,6 +4097,7 @@ dependencies = [ "pin-project-lite", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -3583,16 +4167,19 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ + "base64", "bitflags 2.9.1", "bytes", "futures-util", "http", "http-body", "iri-string", + "mime", "pin-project-lite", "tower 0.5.2", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -3627,6 +4214,8 @@ dependencies = [ "async-trait", "chrono", "config", + "k8s-openapi", + "kube", "nix 0.30.1", "snafu", "tokio", @@ -3797,6 +4386,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicase" version = "2.8.1" @@ -3843,6 +4438,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" @@ -4041,7 +4642,7 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aaf4f3c0ba838e82b4e5ccc4157003fb8c324ee24c058470ffb82820becbde98" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", "jni", "log", "ndk-context", @@ -4099,7 +4700,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings", ] @@ -4132,13 +4733,19 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -4147,7 +4754,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -4195,6 +4802,15 @@ dependencies = [ "windows-targets 0.53.2", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.42.2" diff --git a/crates/tower-cmd/src/run.rs b/crates/tower-cmd/src/run.rs index 92bab124..220a139f 100644 --- a/crates/tower-cmd/src/run.rs +++ b/crates/tower-cmd/src/run.rs @@ -173,7 +173,7 @@ where output::success(&format!("Launching app `{}`", towerfile.app.name)); // Create backend and launcher - use tower_runtime::local::LocalBackend; + use tower_runtime::backends::local::LocalBackend; let backend = LocalBackend::new(config.cache_dir); let mut launcher: AppLauncher = AppLauncher::default(); @@ -601,7 +601,9 @@ async fn monitor_output(mut output: OutputReceiver) { /// monitor_local_status is a helper function that will monitor the status of a given app and waits for /// it to progress to a terminal state. -async fn monitor_handle_status(handle: Arc>) -> Status { +async fn monitor_handle_status( + handle: Arc>, +) -> Status { debug!("Starting status monitoring for execution handle"); let mut check_count = 0; let mut err_count = 0; diff --git a/crates/tower-runtime/Cargo.toml b/crates/tower-runtime/Cargo.toml index aa85e75f..3ed51369 100644 --- a/crates/tower-runtime/Cargo.toml +++ b/crates/tower-runtime/Cargo.toml @@ -18,5 +18,13 @@ tower-telemetry = { workspace = true } tower-uv = { workspace = true } uuid = { workspace = true } +# K8s dependencies (optional) +k8s-openapi = { version = "0.23", features = ["v1_31"], optional = true } +kube = { version = "0.96", features = ["runtime", "derive", "client", "rustls-tls"], default-features = false, optional = true } + +[features] +default = [] +k8s = ["dep:k8s-openapi", "dep:kube"] + [dev-dependencies] config = { workspace = true } diff --git a/crates/tower-runtime/src/backends/k8s.rs b/crates/tower-runtime/src/backends/k8s.rs new file mode 100644 index 00000000..9b54e6af --- /dev/null +++ b/crates/tower-runtime/src/backends/k8s.rs @@ -0,0 +1,607 @@ +//! Kubernetes backend for Tower execution +//! +//! This module provides ExecutionBackend implementation for Kubernetes, supporting: +//! - Pod-based isolation with resource limits +//! - PersistentVolumeClaim-based caching +//! - Service endpoints for long-running apps +//! - Log streaming from pods +//! +//! Only available with the "k8s" feature flag. + +use crate::errors::Error; +use crate::execution::{ + BackendCapabilities, BundleRef, ExecutionBackend, ExecutionHandle, ExecutionSpec, + NetworkingSpec, ServiceEndpoint, +}; +use crate::{Channel, Output, OutputReceiver, Status, FD}; + +use async_trait::async_trait; +use chrono::Utc; +use k8s_openapi::api::core::v1::{ + ConfigMap, Container, Pod, PodSpec, ResourceRequirements, Service, ServicePort, ServiceSpec, + Volume, VolumeMount, +}; +use kube::{ + api::{Api, DeleteParams, LogParams, PostParams}, + runtime::wait::await_condition, + Client, +}; +use std::collections::BTreeMap; +use std::sync::Arc; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::sync::Mutex; +use tokio_util::compat::FuturesAsyncReadCompatExt; + +/// K8sBackend executes apps as Kubernetes Pods +pub struct K8sBackend { + client: Client, + namespace: String, +} + +impl K8sBackend { + /// Create a new K8sBackend + pub async fn new(namespace: String) -> Result { + let client = Client::try_default().await.map_err(|e| { + eprintln!("Failed to create Kubernetes client: {}", e); + Error::RuntimeStartFailed + })?; + + Ok(Self { client, namespace }) + } + + /// Build pod spec from execution spec + fn build_pod_spec( + &self, + spec: &ExecutionSpec, + path_mapping: &BTreeMap, + ) -> Result { + let mut labels = BTreeMap::new(); + labels.insert("app".to_string(), "tower-app".to_string()); + labels.insert("execution-id".to_string(), spec.id.clone()); + + // Build environment variables + let mut env_vars = vec![]; + for (key, value) in &spec.secrets { + env_vars.push(k8s_openapi::api::core::v1::EnvVar { + name: key.clone(), + value: Some(value.clone()), + ..Default::default() + }); + } + for (key, value) in &spec.parameters { + env_vars.push(k8s_openapi::api::core::v1::EnvVar { + name: key.clone(), + value: Some(value.clone()), + ..Default::default() + }); + } + for (key, value) in &spec.env_vars { + env_vars.push(k8s_openapi::api::core::v1::EnvVar { + name: key.clone(), + value: Some(value.clone()), + ..Default::default() + }); + } + + // Build volume mounts + let mut volume_mounts = vec![]; + let mut volumes = vec![]; + + // Build resource requirements + let mut resource_limits = BTreeMap::new(); + let mut resource_requests = BTreeMap::new(); + + if let Some(cpu) = spec.resources.cpu_millicores { + let cpu_str = format!("{}m", cpu); + resource_limits.insert( + "cpu".to_string(), + k8s_openapi::apimachinery::pkg::api::resource::Quantity(cpu_str.clone()), + ); + resource_requests.insert( + "cpu".to_string(), + k8s_openapi::apimachinery::pkg::api::resource::Quantity(cpu_str), + ); + } + + if let Some(memory) = spec.resources.memory_mb { + let mem_str = format!("{}Mi", memory); + resource_limits.insert( + "memory".to_string(), + k8s_openapi::apimachinery::pkg::api::resource::Quantity(mem_str.clone()), + ); + resource_requests.insert( + "memory".to_string(), + k8s_openapi::apimachinery::pkg::api::resource::Quantity(mem_str), + ); + } + + if let Some(storage) = spec.resources.storage_mb { + let storage_str = format!("{}Mi", storage); + resource_limits.insert( + "ephemeral-storage".to_string(), + k8s_openapi::apimachinery::pkg::api::resource::Quantity(storage_str.clone()), + ); + resource_requests.insert( + "ephemeral-storage".to_string(), + k8s_openapi::apimachinery::pkg::api::resource::Quantity(storage_str), + ); + } + + let resources = ResourceRequirements { + limits: Some(resource_limits), + requests: Some(resource_requests), + ..Default::default() + }; + + // Add bundle volume mount + volume_mounts.push(VolumeMount { + name: "bundle".to_string(), + mount_path: "/app".to_string(), + read_only: Some(true), + ..Default::default() + }); + + // Build items array to map ConfigMap keys to their original paths + // e.g., "app__task.py" -> "app/task.py" + let items: Vec = path_mapping + .iter() + .map( + |(sanitized_key, original_path)| k8s_openapi::api::core::v1::KeyToPath { + key: sanitized_key.clone(), + path: original_path.clone(), + mode: Some(0o755), + }, + ) + .collect(); + + // Bundle will be provided as a ConfigMap (created separately) + volumes.push(Volume { + name: "bundle".to_string(), + config_map: Some(k8s_openapi::api::core::v1::ConfigMapVolumeSource { + name: format!("bundle-{}", spec.id), + default_mode: Some(0o755), + items: Some(items), + ..Default::default() + }), + ..Default::default() + }); + + // Build container spec + // Note: In K8s, 'command' = entrypoint, 'args' = command + let container = Container { + name: "app".to_string(), + image: Some(spec.runtime.image.clone()), + env: Some(env_vars), + command: spec.runtime.entrypoint.clone(), // K8s command = entrypoint + args: spec.runtime.command.clone(), // K8s args = command + volume_mounts: if volume_mounts.is_empty() { + None + } else { + Some(volume_mounts) + }, + resources: Some(resources), + working_dir: Some("/app".to_string()), + ..Default::default() + }; + + // Build pod spec + let pod_spec = PodSpec { + containers: vec![container], + volumes: if volumes.is_empty() { + None + } else { + Some(volumes) + }, + restart_policy: Some("Never".to_string()), + ..Default::default() + }; + + Ok(Pod { + metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta { + name: Some(format!("tower-run-{}", spec.id)), + namespace: Some(self.namespace.clone()), + labels: Some(labels), + ..Default::default() + }, + spec: Some(pod_spec), + ..Default::default() + }) + } + + /// Create ConfigMap with bundle contents + /// Returns a mapping of sanitized keys to original paths for volume mounting + async fn create_bundle_configmap( + &self, + spec: &ExecutionSpec, + ) -> Result, Error> { + use k8s_openapi::api::core::v1::ConfigMap; + use std::collections::BTreeMap; + + let configmaps: Api = Api::namespaced(self.client.clone(), &self.namespace); + + // Get bundle path + let bundle_path = match &spec.bundle { + BundleRef::Local { path } => path, + }; + + // Recursively read ALL files from the bundle directory + let mut data = BTreeMap::new(); + let mut binary_data = BTreeMap::new(); + let mut path_mapping = BTreeMap::new(); // sanitized_key -> original_path + + Self::walk_directory( + &bundle_path, + &bundle_path, + &mut data, + &mut binary_data, + &mut path_mapping, + ) + .await?; + + if data.is_empty() && binary_data.is_empty() { + return Err(Error::RuntimeStartFailed); // No files found + } + + let configmap = ConfigMap { + metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta { + name: Some(format!("bundle-{}", spec.id)), + namespace: Some(self.namespace.clone()), + ..Default::default() + }, + data: if !data.is_empty() { Some(data) } else { None }, + binary_data: if !binary_data.is_empty() { + Some(binary_data) + } else { + None + }, + ..Default::default() + }; + + configmaps + .create(&PostParams::default(), &configmap) + .await + .map_err(|_| Error::RuntimeStartFailed)?; + + Ok(path_mapping) + } + + /// Sanitize a file path to be a valid ConfigMap key + /// Replaces '/' with '__' to comply with K8s key restrictions: [-._a-zA-Z0-9]+ + fn sanitize_configmap_key(path: &str) -> String { + path.replace('/', "__") + } + + /// Recursively walk directory and collect all files + async fn walk_directory( + current_path: &std::path::Path, + base_path: &std::path::Path, + text_data: &mut BTreeMap, + binary_data: &mut BTreeMap, + path_mapping: &mut BTreeMap, + ) -> Result<(), Error> { + use tokio::fs; + + let mut entries = fs::read_dir(current_path) + .await + .map_err(|_| Error::RuntimeStartFailed)?; + + while let Some(entry) = entries + .next_entry() + .await + .map_err(|_| Error::RuntimeStartFailed)? + { + let path = entry.path(); + + if path.is_dir() { + // Recursively process subdirectories + Box::pin(Self::walk_directory( + &path, + base_path, + text_data, + binary_data, + path_mapping, + )) + .await?; + } else if path.is_file() { + // Get relative path from base (e.g., "app/task.py") + let relative_path = path + .strip_prefix(base_path) + .map_err(|_| Error::RuntimeStartFailed)? + .to_str() + .ok_or(Error::RuntimeStartFailed)? + .to_string(); + + // Sanitize the key for ConfigMap (e.g., "app/task.py" -> "app__task.py") + let sanitized_key = Self::sanitize_configmap_key(&relative_path); + + // Store mapping for volume mount reconstruction + path_mapping.insert(sanitized_key.clone(), relative_path.clone()); + + // Try reading as text first + match fs::read_to_string(&path).await { + Ok(contents) => { + text_data.insert(sanitized_key, contents); + } + Err(_) => { + // If not text, read as binary + if let Ok(contents) = fs::read(&path).await { + binary_data.insert(sanitized_key, k8s_openapi::ByteString(contents)); + } + } + } + } + } + + Ok(()) + } + + /// Build service spec for networking + fn build_service_spec( + &self, + exec_id: &str, + networking: &NetworkingSpec, + ) -> Result { + let mut labels = BTreeMap::new(); + labels.insert("app".to_string(), "tower-app".to_string()); + labels.insert("execution-id".to_string(), exec_id.to_string()); + + let service_port = ServicePort { + name: Some("http".to_string()), + port: networking.port as i32, + target_port: Some( + k8s_openapi::apimachinery::pkg::util::intstr::IntOrString::Int( + networking.port as i32, + ), + ), + ..Default::default() + }; + + let service_spec = ServiceSpec { + selector: Some(labels.clone()), + ports: Some(vec![service_port]), + type_: Some("ClusterIP".to_string()), + ..Default::default() + }; + + Ok(Service { + metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta { + name: Some( + networking + .service_name + .clone() + .unwrap_or_else(|| format!("tower-svc-{}", exec_id)), + ), + namespace: Some(self.namespace.clone()), + labels: Some(labels), + ..Default::default() + }, + spec: Some(service_spec), + ..Default::default() + }) + } +} + +#[async_trait] +impl ExecutionBackend for K8sBackend { + type Handle = K8sHandle; + + async fn create(&self, spec: ExecutionSpec) -> Result { + let pods: Api = Api::namespaced(self.client.clone(), &self.namespace); + + // Create ConfigMap with bundle contents and get path mapping + let path_mapping = self.create_bundle_configmap(&spec).await?; + + // Build and create pod with path mapping for volume items + let pod = self.build_pod_spec(&spec, &path_mapping)?; + let pod_name = pod.metadata.name.clone().ok_or(Error::RuntimeStartFailed)?; + + pods.create(&PostParams::default(), &pod) + .await + .map_err(|_| Error::RuntimeStartFailed)?; + + // Create service if networking is specified + let service_endpoint = if let Some(networking) = &spec.networking { + if networking.expose_service { + let services: Api = Api::namespaced(self.client.clone(), &self.namespace); + let service = self.build_service_spec(&spec.id, networking)?; + let service_name = service + .metadata + .name + .clone() + .ok_or(Error::RuntimeStartFailed)?; + + services + .create(&PostParams::default(), &service) + .await + .map_err(|_| Error::RuntimeStartFailed)?; + + Some(ServiceEndpoint { + host: format!("{}.{}.svc.cluster.local", service_name, self.namespace), + port: networking.port, + protocol: "http".to_string(), + url: Some(format!( + "http://{}.{}.svc.cluster.local:{}", + service_name, self.namespace, networking.port + )), + }) + } else { + None + } + } else { + None + }; + + Ok(K8sHandle { + id: spec.id, + pod_name, + namespace: self.namespace.clone(), + client: self.client.clone(), + service_endpoint: Arc::new(Mutex::new(service_endpoint)), + }) + } + + fn capabilities(&self) -> BackendCapabilities { + BackendCapabilities { + name: "k8s".to_string(), + supports_persistent_cache: true, + supports_prewarming: true, + supports_network_isolation: true, + supports_service_endpoints: true, + typical_cold_start_ms: 5000, // ~5s for image pull + pod start + typical_warm_start_ms: 1000, // ~1s with cached image + max_concurrent_executions: None, // Limited by cluster capacity + } + } + + async fn cleanup(&self) -> Result<(), Error> { + // No global cleanup needed for K8s backend + Ok(()) + } +} + +/// K8sHandle provides lifecycle management for a Kubernetes Pod execution +pub struct K8sHandle { + id: String, + pod_name: String, + namespace: String, + client: Client, + service_endpoint: Arc>>, +} + +#[async_trait] +impl ExecutionHandle for K8sHandle { + fn id(&self) -> &str { + &self.id + } + + async fn status(&self) -> Result { + let pods: Api = Api::namespaced(self.client.clone(), &self.namespace); + + let pod = pods + .get(&self.pod_name) + .await + .map_err(|_| Error::NoRunningApp)?; + + let phase = pod + .status + .and_then(|s| s.phase) + .unwrap_or_else(|| "Unknown".to_string()); + + Ok(match phase.as_str() { + "Pending" => Status::None, + "Running" => Status::Running, + "Succeeded" => Status::Exited, + "Failed" => Status::Crashed { code: 1 }, + _ => Status::None, + }) + } + + async fn logs(&self) -> Result { + let pods: Api = Api::namespaced(self.client.clone(), &self.namespace); + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + + let pod_name = self.pod_name.clone(); + let pods_clone = pods.clone(); + + tokio::spawn(async move { + // Wait for pod to have containers created (Running, Succeeded, or Failed) + // This ensures we can stream logs even if the pod crashes + let condition = await_condition(pods_clone.clone(), &pod_name, |obj: Option<&Pod>| { + obj.and_then(|pod| pod.status.as_ref()) + .and_then(|status| status.phase.as_ref()) + .map(|phase| phase == "Running" || phase == "Succeeded" || phase == "Failed") + .unwrap_or(false) + }); + + // Wait with a timeout + if tokio::time::timeout(std::time::Duration::from_secs(60), condition) + .await + .is_ok() + { + let log_params = LogParams { + follow: true, + ..Default::default() + }; + + if let Ok(logs) = pods_clone.log_stream(&pod_name, &log_params).await { + // Convert futures AsyncBufRead to tokio AsyncRead + let compat_logs = logs.compat(); + let mut reader = BufReader::new(compat_logs).lines(); + while let Ok(Some(line)) = reader.next_line().await { + let output = Output { + time: Utc::now(), + fd: FD::Stdout, // K8s combines stdout/stderr + channel: Channel::Program, + line, + }; + if tx.send(output).is_err() { + break; + } + } + } + } + }); + + Ok(rx) + } + + async fn terminate(&mut self) -> Result<(), Error> { + let pods: Api = Api::namespaced(self.client.clone(), &self.namespace); + + pods.delete(&self.pod_name, &DeleteParams::default()) + .await + .map_err(|_| Error::TerminateFailed)?; + + Ok(()) + } + + async fn kill(&mut self) -> Result<(), Error> { + // For K8s, kill is the same as terminate (pod deletion) + self.terminate().await + } + + async fn wait_for_completion(&self) -> Result { + let pods: Api = Api::namespaced(self.client.clone(), &self.namespace); + + // Wait for pod to reach terminal state + await_condition(pods.clone(), &self.pod_name, |obj: Option<&Pod>| { + obj.and_then(|pod| pod.status.as_ref()) + .and_then(|status| status.phase.as_ref()) + .map(|phase| phase == "Succeeded" || phase == "Failed") + .unwrap_or(false) + }) + .await + .map_err(|_| Error::Timeout)?; + + self.status().await + } + + async fn service_endpoint(&self) -> Result, Error> { + let endpoint = self.service_endpoint.lock().await; + Ok(endpoint.clone()) + } + + async fn cleanup(&mut self) -> Result<(), Error> { + // Delete pod + self.terminate().await?; + + // Delete ConfigMap with bundle + let configmaps: Api = Api::namespaced(self.client.clone(), &self.namespace); + let configmap_name = format!("bundle-{}", self.id); + let _ = configmaps + .delete(&configmap_name, &DeleteParams::default()) + .await; + + // Delete service if it exists + if let Some(endpoint) = self.service_endpoint.lock().await.as_ref() { + let services: Api = Api::namespaced(self.client.clone(), &self.namespace); + // Extract service name from hostname + let service_name = endpoint.host.split('.').next().unwrap_or("unknown"); + let _ = services + .delete(service_name, &DeleteParams::default()) + .await; + } + + Ok(()) + } +} diff --git a/crates/tower-runtime/src/backends/local.rs b/crates/tower-runtime/src/backends/local.rs new file mode 100644 index 00000000..c0eb6478 --- /dev/null +++ b/crates/tower-runtime/src/backends/local.rs @@ -0,0 +1,155 @@ +//! Local subprocess execution backend + +use crate::errors::Error; +use crate::execution::{ + BackendCapabilities, BundleRef, CacheBackend, ExecutionBackend, ExecutionHandle, ExecutionSpec, + ServiceEndpoint, +}; +use crate::local::LocalApp; +use crate::{App, OutputReceiver, StartOptions, Status}; + +use async_trait::async_trait; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::Mutex; +use tokio::time::Duration; +use tower_package::Package; + +/// LocalBackend executes apps as local subprocesses +pub struct LocalBackend { + /// Optional default cache directory to use + cache_dir: Option, +} + +impl LocalBackend { + pub fn new(cache_dir: Option) -> Self { + Self { cache_dir } + } +} + +#[async_trait] +impl ExecutionBackend for LocalBackend { + type Handle = LocalHandle; + + async fn create(&self, spec: ExecutionSpec) -> Result { + // Convert ExecutionSpec to StartOptions for LocalApp + let (output_sender, output_receiver) = tokio::sync::mpsc::unbounded_channel(); + + // Get cache_dir from spec or use backend default + let cache_dir = match &spec.runtime.cache.backend { + CacheBackend::Local { cache_dir } => Some(cache_dir.clone()), + _ => self.cache_dir.clone(), + }; + + let opts = StartOptions { + ctx: spec.telemetry_ctx, + package: match spec.bundle { + BundleRef::Local { path } => Package::from_unpacked_path(path).await, + }, + cwd: None, // LocalApp determines cwd from package + environment: spec.environment, + secrets: spec.secrets, + parameters: spec.parameters, + env_vars: spec.env_vars, + output_sender: output_sender.clone(), + cache_dir, + }; + + // Start the LocalApp + let app = LocalApp::start(opts).await?; + + Ok(LocalHandle { + id: spec.id, + app: Arc::new(Mutex::new(app)), + output_receiver: Arc::new(Mutex::new(output_receiver)), + }) + } + + fn capabilities(&self) -> BackendCapabilities { + BackendCapabilities { + name: "local".to_string(), + supports_persistent_cache: true, + supports_prewarming: false, + supports_network_isolation: false, + supports_service_endpoints: false, + typical_cold_start_ms: 1000, // ~1s for venv + sync + typical_warm_start_ms: 100, // ~100ms with warm cache + max_concurrent_executions: None, // Limited by system resources + } + } + + async fn cleanup(&self) -> Result<(), Error> { + // Nothing to cleanup for local backend + Ok(()) + } +} + +/// LocalHandle provides lifecycle management for a local subprocess execution +pub struct LocalHandle { + id: String, + app: Arc>, + output_receiver: Arc>, +} + +#[async_trait] +impl ExecutionHandle for LocalHandle { + fn id(&self) -> &str { + &self.id + } + + async fn status(&self) -> Result { + let app = self.app.lock().await; + app.status().await + } + + async fn logs(&self) -> Result { + // Create a new channel for log streaming + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + + // Spawn a task to forward Output from the internal receiver + let output_receiver = self.output_receiver.clone(); + tokio::spawn(async move { + let mut receiver = output_receiver.lock().await; + while let Some(output) = receiver.recv().await { + if tx.send(output).is_err() { + break; // Receiver dropped + } + } + }); + + Ok(rx) + } + + async fn terminate(&mut self) -> Result<(), Error> { + let mut app = self.app.lock().await; + app.terminate().await + } + + async fn kill(&mut self) -> Result<(), Error> { + // For local processes, kill is the same as terminate + self.terminate().await + } + + async fn wait_for_completion(&self) -> Result { + loop { + let status = self.status().await?; + match status { + Status::None | Status::Running => { + tokio::time::sleep(Duration::from_millis(100)).await; + } + _ => return Ok(status), + } + } + } + + async fn service_endpoint(&self) -> Result, Error> { + // Local backend doesn't support service endpoints + Ok(None) + } + + async fn cleanup(&mut self) -> Result<(), Error> { + // Ensure the app is terminated + self.terminate().await?; + Ok(()) + } +} diff --git a/crates/tower-runtime/src/backends/mod.rs b/crates/tower-runtime/src/backends/mod.rs new file mode 100644 index 00000000..894d4508 --- /dev/null +++ b/crates/tower-runtime/src/backends/mod.rs @@ -0,0 +1,6 @@ +//! Concrete implementations of ExecutionBackend for different compute substrates + +pub mod local; + +#[cfg(feature = "k8s")] +pub mod k8s; diff --git a/crates/tower-runtime/src/lib.rs b/crates/tower-runtime/src/lib.rs index 162910c6..48ecd9da 100644 --- a/crates/tower-runtime/src/lib.rs +++ b/crates/tower-runtime/src/lib.rs @@ -7,6 +7,7 @@ use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use tower_package::Package; use tower_telemetry::debug; +pub mod backends; pub mod errors; pub mod execution; pub mod local; diff --git a/crates/tower-runtime/src/local.rs b/crates/tower-runtime/src/local.rs index e003a462..c3d1bffa 100644 --- a/crates/tower-runtime/src/local.rs +++ b/crates/tower-runtime/src/local.rs @@ -6,7 +6,7 @@ use std::process::Stdio; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; -use crate::{errors::Error, OutputReceiver, OutputSender, StartOptions, Status}; +use crate::{errors::Error, OutputSender, StartOptions, Status}; use tokio::{ fs, @@ -324,7 +324,7 @@ impl Drop for LocalApp { } impl App for LocalApp { - type Backend = LocalBackend; + type Backend = crate::backends::local::LocalBackend; async fn start(opts: StartOptions) -> Result { let terminator = CancellationToken::new(); @@ -574,153 +574,3 @@ async fn drain_output( fn is_bash_package(package: &Package) -> bool { return package.manifest.invoke.ends_with(".sh"); } - -// ============================================================================ -// LocalBackend - ExecutionBackend implementation for local subprocess execution -// ============================================================================ - -use crate::execution::{ - BackendCapabilities, BundleRef, CacheBackend, ExecutionBackend, ExecutionHandle, ExecutionSpec, - ServiceEndpoint, -}; -use async_trait::async_trait; -use std::sync::Arc; - -/// LocalBackend executes apps as local subprocesses -pub struct LocalBackend { - /// Optional default cache directory to use - cache_dir: Option, -} - -impl LocalBackend { - pub fn new(cache_dir: Option) -> Self { - Self { cache_dir } - } -} - -#[async_trait] -impl ExecutionBackend for LocalBackend { - type Handle = LocalHandle; - - async fn create(&self, spec: ExecutionSpec) -> Result { - // Convert ExecutionSpec to StartOptions for LocalApp - let (output_sender, output_receiver) = tokio::sync::mpsc::unbounded_channel(); - - // Get cache_dir from spec or use backend default - let cache_dir = match &spec.runtime.cache.backend { - CacheBackend::Local { cache_dir } => Some(cache_dir.clone()), - _ => self.cache_dir.clone(), - }; - - let opts = StartOptions { - ctx: spec.telemetry_ctx, - package: match spec.bundle { - BundleRef::Local { path } => Package::from_unpacked_path(path).await, - }, - cwd: None, // LocalApp determines cwd from package - environment: spec.environment, - secrets: spec.secrets, - parameters: spec.parameters, - env_vars: spec.env_vars, - output_sender: output_sender.clone(), - cache_dir, - }; - - // Start the LocalApp - let app = LocalApp::start(opts).await?; - - Ok(LocalHandle { - id: spec.id, - app: Arc::new(Mutex::new(app)), - output_receiver: Arc::new(Mutex::new(output_receiver)), - }) - } - - fn capabilities(&self) -> BackendCapabilities { - BackendCapabilities { - name: "local".to_string(), - supports_persistent_cache: true, - supports_prewarming: false, - supports_network_isolation: false, - supports_service_endpoints: false, - typical_cold_start_ms: 1000, // ~1s for venv + sync - typical_warm_start_ms: 100, // ~100ms with warm cache - max_concurrent_executions: None, // Limited by system resources - } - } - - async fn cleanup(&self) -> Result<(), Error> { - // Nothing to cleanup for local backend - Ok(()) - } -} - -/// LocalHandle provides lifecycle management for a local subprocess execution -pub struct LocalHandle { - id: String, - app: Arc>, - output_receiver: Arc>, -} - -#[async_trait] -impl ExecutionHandle for LocalHandle { - fn id(&self) -> &str { - &self.id - } - - async fn status(&self) -> Result { - let app = self.app.lock().await; - app.status().await - } - - async fn logs(&self) -> Result { - // Create a new channel for log streaming - let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); - - // Spawn a task to forward Output from the internal receiver - let output_receiver = self.output_receiver.clone(); - tokio::spawn(async move { - let mut receiver = output_receiver.lock().await; - while let Some(output) = receiver.recv().await { - if tx.send(output).is_err() { - break; // Receiver dropped - } - } - }); - - Ok(rx) - } - - async fn terminate(&mut self) -> Result<(), Error> { - let mut app = self.app.lock().await; - app.terminate().await - } - - async fn kill(&mut self) -> Result<(), Error> { - // For local processes, kill is the same as terminate - self.terminate().await - } - - async fn wait_for_completion(&self) -> Result { - loop { - let status = self.status().await?; - match status { - Status::None | Status::Running => { - tokio::time::sleep(Duration::from_millis(100)).await; - } - _ => return Ok(status), - } - } - } - - async fn service_endpoint(&self) -> Result, Error> { - // Local backend doesn't support service endpoints - Ok(None) - } - - async fn cleanup(&mut self) -> Result<(), Error> { - // Ensure the app is terminated - self.terminate().await?; - Ok(()) - } -} From ef57bf7929500bd97de285864b5015c3fe5e4fb8 Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Tue, 13 Jan 2026 17:17:10 +0100 Subject: [PATCH 08/21] minor --- crates/tower-runtime/src/lib.rs | 6 +++--- crates/tower-runtime/src/local.rs | 24 ++++++++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/crates/tower-runtime/src/lib.rs b/crates/tower-runtime/src/lib.rs index 48ecd9da..1f57e8a6 100644 --- a/crates/tower-runtime/src/lib.rs +++ b/crates/tower-runtime/src/lib.rs @@ -51,7 +51,7 @@ pub type OutputReceiver = UnboundedReceiver; pub type OutputSender = UnboundedSender; pub trait App: Send + Sync { - type Backend: execution::ExecutionBackend; + type Backend: ExecutionBackend; // start will start the process fn start(opts: StartOptions) -> impl Future> + Send @@ -66,7 +66,7 @@ pub trait App: Send + Sync { } pub struct AppLauncher { - pub handle: Option<::Handle>, + pub handle: Option<::Handle>, } impl std::default::Default for AppLauncher { @@ -77,7 +77,7 @@ impl std::default::Default for AppLauncher { impl AppLauncher where - A::Backend: execution::ExecutionBackend, + A::Backend: ExecutionBackend, { pub async fn launch( &mut self, diff --git a/crates/tower-runtime/src/local.rs b/crates/tower-runtime/src/local.rs index c3d1bffa..98da37e6 100644 --- a/crates/tower-runtime/src/local.rs +++ b/crates/tower-runtime/src/local.rs @@ -343,6 +343,18 @@ impl App for LocalApp { }) } + async fn terminate(&mut self) -> Result<(), Error> { + self.terminator.cancel(); + + // Now we should wait for the join handle to finish. + if let Some(execute_handle) = self.execute_handle.take() { + let _ = execute_handle.await; + self.execute_handle = None; + } + + Ok(()) + } + async fn status(&self) -> Result { let mut status = self.status.lock().await; @@ -369,18 +381,6 @@ impl App for LocalApp { } } } - - async fn terminate(&mut self) -> Result<(), Error> { - self.terminator.cancel(); - - // Now we should wait for the join handle to finish. - if let Some(execute_handle) = self.execute_handle.take() { - let _ = execute_handle.await; - self.execute_handle = None; - } - - Ok(()) - } } async fn execute_bash_program( From 7432ed6b4111904dceff4f74d9d704737a1fe53f Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Tue, 13 Jan 2026 17:52:47 +0100 Subject: [PATCH 09/21] Make AppLauncher generic over ExecutionBackend instead of app --- crates/tower-cmd/src/run.rs | 6 ++---- crates/tower-runtime/src/lib.rs | 15 +++++---------- crates/tower-runtime/src/local.rs | 2 -- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/crates/tower-cmd/src/run.rs b/crates/tower-cmd/src/run.rs index 220a139f..a6aa08f6 100644 --- a/crates/tower-cmd/src/run.rs +++ b/crates/tower-cmd/src/run.rs @@ -5,9 +5,7 @@ use std::collections::HashMap; use std::path::PathBuf; use tower_api::models::Run; use tower_package::{Package, PackageSpec}; -use tower_runtime::{ - execution::ExecutionHandle, local::LocalApp, AppLauncher, OutputReceiver, Status, -}; +use tower_runtime::{execution::ExecutionHandle, AppLauncher, OutputReceiver, Status}; use tower_telemetry::{debug, Context}; use std::sync::Arc; @@ -175,7 +173,7 @@ where // Create backend and launcher use tower_runtime::backends::local::LocalBackend; let backend = LocalBackend::new(config.cache_dir); - let mut launcher: AppLauncher = AppLauncher::default(); + let mut launcher: AppLauncher = AppLauncher::default(); launcher .launch( diff --git a/crates/tower-runtime/src/lib.rs b/crates/tower-runtime/src/lib.rs index 1f57e8a6..e51db046 100644 --- a/crates/tower-runtime/src/lib.rs +++ b/crates/tower-runtime/src/lib.rs @@ -51,8 +51,6 @@ pub type OutputReceiver = UnboundedReceiver; pub type OutputSender = UnboundedSender; pub trait App: Send + Sync { - type Backend: ExecutionBackend; - // start will start the process fn start(opts: StartOptions) -> impl Future> + Send where @@ -65,23 +63,20 @@ pub trait App: Send + Sync { fn status(&self) -> impl Future> + Send; } -pub struct AppLauncher { - pub handle: Option<::Handle>, +pub struct AppLauncher { + pub handle: Option, } -impl std::default::Default for AppLauncher { +impl Default for AppLauncher { fn default() -> Self { Self { handle: None } } } -impl AppLauncher -where - A::Backend: ExecutionBackend, -{ +impl AppLauncher { pub async fn launch( &mut self, - backend: A::Backend, + backend: B, ctx: tower_telemetry::Context, package: Package, environment: String, diff --git a/crates/tower-runtime/src/local.rs b/crates/tower-runtime/src/local.rs index 98da37e6..57d15ba0 100644 --- a/crates/tower-runtime/src/local.rs +++ b/crates/tower-runtime/src/local.rs @@ -324,8 +324,6 @@ impl Drop for LocalApp { } impl App for LocalApp { - type Backend = crate::backends::local::LocalBackend; - async fn start(opts: StartOptions) -> Result { let terminator = CancellationToken::new(); From d741dac77599feef0975064b977c77e800c759a0 Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Wed, 14 Jan 2026 11:08:08 +0100 Subject: [PATCH 10/21] Renaming local -> subprocess --- crates/tower-cmd/src/run.rs | 8 ++++---- crates/tower-runtime/src/backends/mod.rs | 2 +- .../src/backends/{local.rs => subprocess.rs} | 20 +++++++++---------- crates/tower-runtime/src/execution.rs | 2 -- 4 files changed, 15 insertions(+), 17 deletions(-) rename crates/tower-runtime/src/backends/{local.rs => subprocess.rs} (91%) diff --git a/crates/tower-cmd/src/run.rs b/crates/tower-cmd/src/run.rs index a6aa08f6..ce9105c6 100644 --- a/crates/tower-cmd/src/run.rs +++ b/crates/tower-cmd/src/run.rs @@ -171,9 +171,9 @@ where output::success(&format!("Launching app `{}`", towerfile.app.name)); // Create backend and launcher - use tower_runtime::backends::local::LocalBackend; - let backend = LocalBackend::new(config.cache_dir); - let mut launcher: AppLauncher = AppLauncher::default(); + use tower_runtime::backends::subprocess::SubprocessBackend; + let backend = SubprocessBackend::new(config.cache_dir); + let mut launcher: AppLauncher = AppLauncher::default(); launcher .launch( @@ -600,7 +600,7 @@ async fn monitor_output(mut output: OutputReceiver) { /// monitor_local_status is a helper function that will monitor the status of a given app and waits for /// it to progress to a terminal state. async fn monitor_handle_status( - handle: Arc>, + handle: Arc>, ) -> Status { debug!("Starting status monitoring for execution handle"); let mut check_count = 0; diff --git a/crates/tower-runtime/src/backends/mod.rs b/crates/tower-runtime/src/backends/mod.rs index 894d4508..d859ade5 100644 --- a/crates/tower-runtime/src/backends/mod.rs +++ b/crates/tower-runtime/src/backends/mod.rs @@ -1,6 +1,6 @@ //! Concrete implementations of ExecutionBackend for different compute substrates -pub mod local; +pub mod subprocess; #[cfg(feature = "k8s")] pub mod k8s; diff --git a/crates/tower-runtime/src/backends/local.rs b/crates/tower-runtime/src/backends/subprocess.rs similarity index 91% rename from crates/tower-runtime/src/backends/local.rs rename to crates/tower-runtime/src/backends/subprocess.rs index c0eb6478..b2d65806 100644 --- a/crates/tower-runtime/src/backends/local.rs +++ b/crates/tower-runtime/src/backends/subprocess.rs @@ -1,4 +1,4 @@ -//! Local subprocess execution backend +//! Subprocess execution backend use crate::errors::Error; use crate::execution::{ @@ -15,21 +15,21 @@ use tokio::sync::Mutex; use tokio::time::Duration; use tower_package::Package; -/// LocalBackend executes apps as local subprocesses -pub struct LocalBackend { +/// SubprocessBackend executes apps as a subprocess +pub struct SubprocessBackend { /// Optional default cache directory to use cache_dir: Option, } -impl LocalBackend { +impl SubprocessBackend { pub fn new(cache_dir: Option) -> Self { Self { cache_dir } } } #[async_trait] -impl ExecutionBackend for LocalBackend { - type Handle = LocalHandle; +impl ExecutionBackend for SubprocessBackend { + type Handle = SubprocessHandle; async fn create(&self, spec: ExecutionSpec) -> Result { // Convert ExecutionSpec to StartOptions for LocalApp @@ -58,7 +58,7 @@ impl ExecutionBackend for LocalBackend { // Start the LocalApp let app = LocalApp::start(opts).await?; - Ok(LocalHandle { + Ok(SubprocessHandle { id: spec.id, app: Arc::new(Mutex::new(app)), output_receiver: Arc::new(Mutex::new(output_receiver)), @@ -84,15 +84,15 @@ impl ExecutionBackend for LocalBackend { } } -/// LocalHandle provides lifecycle management for a local subprocess execution -pub struct LocalHandle { +/// SubprocessHandle provides lifecycle management for a subprocess execution +pub struct SubprocessHandle { id: String, app: Arc>, output_receiver: Arc>, } #[async_trait] -impl ExecutionHandle for LocalHandle { +impl ExecutionHandle for SubprocessHandle { fn id(&self) -> &str { &self.id } diff --git a/crates/tower-runtime/src/execution.rs b/crates/tower-runtime/src/execution.rs index b84ed7fd..76bb0481 100644 --- a/crates/tower-runtime/src/execution.rs +++ b/crates/tower-runtime/src/execution.rs @@ -248,5 +248,3 @@ pub struct ServiceEndpoint { /// Full URL if applicable (e.g., "http://app-run-123.default.svc.cluster.local:8080") pub url: Option, } - -// LocalBackend implemented in local.rs From 26369da358bd25984c64334c78dcdbfe610cdf8e Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Wed, 14 Jan 2026 11:30:56 +0100 Subject: [PATCH 11/21] CliBackend --- crates/tower-cmd/src/run.rs | 43 ++++----- crates/tower-runtime/src/backends/cli.rs | 90 +++++++++++++++++++ crates/tower-runtime/src/backends/mod.rs | 3 + .../test-home/.config/tower/session.json | 21 +---- 4 files changed, 112 insertions(+), 45 deletions(-) create mode 100644 crates/tower-runtime/src/backends/cli.rs diff --git a/crates/tower-cmd/src/run.rs b/crates/tower-cmd/src/run.rs index ce9105c6..55b33e02 100644 --- a/crates/tower-cmd/src/run.rs +++ b/crates/tower-cmd/src/run.rs @@ -5,11 +5,12 @@ use std::collections::HashMap; use std::path::PathBuf; use tower_api::models::Run; use tower_package::{Package, PackageSpec}; -use tower_runtime::{execution::ExecutionHandle, AppLauncher, OutputReceiver, Status}; +use tower_runtime::{OutputReceiver, Status}; use tower_telemetry::{debug, Context}; use std::sync::Arc; use tokio::sync::{ + mpsc::unbounded_channel, mpsc::Receiver as MpscReceiver, oneshot::{self, Receiver as OneshotReceiver}, Mutex, @@ -168,17 +169,19 @@ where // Unpack the package package.unpack().await?; - output::success(&format!("Launching app `{}`", towerfile.app.name)); + // Create output channel - simple pattern for CLI + let (sender, receiver) = unbounded_channel(); - // Create backend and launcher - use tower_runtime::backends::subprocess::SubprocessBackend; - let backend = SubprocessBackend::new(config.cache_dir); - let mut launcher: AppLauncher = AppLauncher::default(); + output::success(&format!("Launching app `{}`", towerfile.app.name)); + let output_task = tokio::spawn(output_handler(receiver)); - launcher + // Create backend and launch app + use tower_runtime::backends::cli::CliBackend; + let backend = CliBackend::new(config.cache_dir); + let handle = backend .launch( - backend, Context::new(), + sender, package, env.to_string(), secrets, @@ -187,13 +190,9 @@ where ) .await?; - // Get logs from handle and spawn output handler - let logs_receiver = launcher.handle.as_ref().unwrap().logs().await?; - let output_task = tokio::spawn(output_handler(logs_receiver)); - // Monitor app status concurrently - let handle = Arc::new(Mutex::new(launcher.handle.take().unwrap())); - let status_task = tokio::spawn(monitor_handle_status(Arc::clone(&handle))); + let handle = Arc::new(Mutex::new(handle)); + let status_task = tokio::spawn(monitor_cli_status(Arc::clone(&handle))); // Wait for app to complete or SIGTERM let status_result = tokio::select! { @@ -599,10 +598,8 @@ async fn monitor_output(mut output: OutputReceiver) { /// monitor_local_status is a helper function that will monitor the status of a given app and waits for /// it to progress to a terminal state. -async fn monitor_handle_status( - handle: Arc>, -) -> Status { - debug!("Starting status monitoring for execution handle"); +async fn monitor_cli_status(handle: Arc>) -> Status { + debug!("Starting status monitoring for CLI execution"); let mut check_count = 0; let mut err_count = 0; @@ -610,11 +607,11 @@ async fn monitor_handle_status( check_count += 1; debug!( - "Status check #{}, attempting to get handle status", + "Status check #{}, attempting to get CLI handle status", check_count ); - match tower_runtime::execution::ExecutionHandle::status(&*handle.lock().await).await { + match handle.lock().await.status().await { Ok(status) => { // We reset the error count to indicate that we can intermittently get statuses. err_count = 0; @@ -622,14 +619,10 @@ async fn monitor_handle_status( match status { Status::Exited => { debug!("Run exited cleanly, stopping status monitoring"); - - // We're done. Exit this loop and function. return status; } Status::Crashed { .. } => { debug!("Run crashed, stopping status monitoring"); - - // We're done. Exit this loop and function. return status; } _ => { @@ -646,7 +639,7 @@ async fn monitor_handle_status( if err_count >= 5 { debug!("Failed to get handle status after 5 attempts, giving up"); output::error("An error occured while monitoring your local run status!"); - return tower_runtime::Status::Crashed { code: -1 }; + return Status::Crashed { code: -1 }; } // Otherwise, keep on keepin' on. diff --git a/crates/tower-runtime/src/backends/cli.rs b/crates/tower-runtime/src/backends/cli.rs new file mode 100644 index 00000000..537bc589 --- /dev/null +++ b/crates/tower-runtime/src/backends/cli.rs @@ -0,0 +1,90 @@ +//! Simple backend for CLI --local runs +//! +//! This backend follows a simpler pattern than SubprocessBackend: +//! - The caller creates the output channel and passes the sender +//! - The caller keeps the receiver for direct consumption +//! - No need for complex handle.logs() method +//! - Single consumer model (perfect for CLI) + +use crate::errors::Error; +use crate::local::LocalApp; +use crate::{App, OutputSender, StartOptions, Status}; +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::Mutex; +use tower_package::Package; + +/// CliBackend executes apps as local subprocesses for CLI --local runs +pub struct CliBackend { + /// Optional default cache directory to use + cache_dir: Option, +} + +impl CliBackend { + pub fn new(cache_dir: Option) -> Self { + Self { cache_dir } + } + + /// Launch an app with the given parameters + /// + /// Unlike SubprocessBackend.create(), this takes OutputSender directly + /// so the caller can immediately start consuming output from their receiver. + pub async fn launch( + &self, + ctx: tower_telemetry::Context, + output_sender: OutputSender, + package: Package, + environment: String, + secrets: HashMap, + parameters: HashMap, + env_vars: HashMap, + ) -> Result { + let opts = StartOptions { + ctx, + package, + cwd: None, // LocalApp determines cwd from package + environment, + secrets, + parameters, + env_vars, + output_sender, + cache_dir: self.cache_dir.clone(), + }; + + // Start the LocalApp + let app = LocalApp::start(opts).await?; + + Ok(CliHandle { + app: Arc::new(Mutex::new(Some(app))), + }) + } +} + +/// CliHandle provides lifecycle management for a CLI local subprocess execution +pub struct CliHandle { + app: Arc>>, +} + +impl CliHandle { + /// Get current execution status + pub async fn status(&self) -> Result { + let guard = self.app.lock().await; + if let Some(app) = guard.as_ref() { + app.status().await + } else { + // App has already been terminated + Ok(Status::Crashed { code: -1 }) + } + } + + /// Terminate execution gracefully + pub async fn terminate(&mut self) -> Result<(), Error> { + let mut guard = self.app.lock().await; + if let Some(mut app) = guard.take() { + app.terminate().await + } else { + Ok(()) + } + } +} diff --git a/crates/tower-runtime/src/backends/mod.rs b/crates/tower-runtime/src/backends/mod.rs index d859ade5..b5cc4e3b 100644 --- a/crates/tower-runtime/src/backends/mod.rs +++ b/crates/tower-runtime/src/backends/mod.rs @@ -4,3 +4,6 @@ pub mod subprocess; #[cfg(feature = "k8s")] pub mod k8s; + +/// Simple backend for CLI --local runs +pub mod cli; diff --git a/tests/integration/test-home/.config/tower/session.json b/tests/integration/test-home/.config/tower/session.json index f2590a76..469218e4 100644 --- a/tests/integration/test-home/.config/tower/session.json +++ b/tests/integration/test-home/.config/tower/session.json @@ -1,20 +1 @@ -{ - "tower_url": "http://127.0.0.1:8000", - "user": { - "email": "test@example.com", - "first_name": "Test", - "last_name": "User", - "created_at": "2023-01-01T00:00:00Z" - }, - "token": { - "jwt": "mock_jwt_token" - }, - "active_team": { - "name": "test-team", - "token": { - "jwt": "mock_jwt_token" - }, - "team_type": "personal" - }, - "teams": [] -} \ No newline at end of file +{"tower_url":"http://127.0.0.1:8000/v1","user":{"email":"test@example.com","first_name":"Test","last_name":"User","created_at":"2023-01-01T00:00:00Z"},"token":{"jwt":"mock_jwt_token"},"active_team":{"name":"default","token":{"jwt":"mock_jwt_token"},"team_type":"user"},"teams":[{"name":"default","token":{"jwt":"mock_jwt_token"},"team_type":"user"}]} \ No newline at end of file From 948d9017dd7b3d774b1c4b0b849ad6c94cdb46fe Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Wed, 14 Jan 2026 11:39:24 +0100 Subject: [PATCH 12/21] remove AppLauncher --- crates/tower-runtime/src/lib.rs | 103 -------------------------------- 1 file changed, 103 deletions(-) diff --git a/crates/tower-runtime/src/lib.rs b/crates/tower-runtime/src/lib.rs index e51db046..587c2ac6 100644 --- a/crates/tower-runtime/src/lib.rs +++ b/crates/tower-runtime/src/lib.rs @@ -5,7 +5,6 @@ use std::path::PathBuf; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use tower_package::Package; -use tower_telemetry::debug; pub mod backends; pub mod errors; @@ -13,7 +12,6 @@ pub mod execution; pub mod local; use errors::Error; -use execution::{ExecutionBackend, ExecutionHandle}; #[derive(Copy, Clone)] pub enum FD { @@ -63,107 +61,6 @@ pub trait App: Send + Sync { fn status(&self) -> impl Future> + Send; } -pub struct AppLauncher { - pub handle: Option, -} - -impl Default for AppLauncher { - fn default() -> Self { - Self { handle: None } - } -} - -impl AppLauncher { - pub async fn launch( - &mut self, - backend: B, - ctx: tower_telemetry::Context, - package: Package, - environment: String, - secrets: HashMap, - parameters: HashMap, - env_vars: HashMap, - ) -> Result<(), Error> { - let package_path = package.unpacked_path.clone().unwrap().to_path_buf(); - - // Build ExecutionSpec from parameters - use std::time::{SystemTime, UNIX_EPOCH}; - let id = format!( - "run-{}", - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos() - ); - - let spec = execution::ExecutionSpec { - id, - bundle: execution::BundleRef::Local { - path: package_path.clone(), - }, - runtime: execution::RuntimeConfig { - image: "local".to_string(), - version: None, - cache: execution::CacheConfig { - enable_bundle_cache: true, - enable_runtime_cache: true, - enable_dependency_cache: true, - backend: execution::CacheBackend::None, - isolation: execution::CacheIsolation::None, - }, - entrypoint: None, - command: None, - }, - environment, - secrets, - parameters, - env_vars, - resources: execution::ResourceLimits { - cpu_millicores: None, - memory_mb: None, - storage_mb: None, - max_pids: None, - gpu_count: 0, - timeout_seconds: 3600, - }, - networking: None, - telemetry_ctx: ctx, - }; - - // Drop any existing handle - self.handle = None; - - // Create execution using backend - let handle = backend.create(spec).await?; - self.handle = Some(handle); - - Ok(()) - } - - pub async fn terminate(&mut self) -> Result<(), Error> { - if let Some(handle) = &mut self.handle { - if let Err(err) = handle.terminate().await { - debug!("failed to terminate app: {}", err); - Err(err) - } else { - self.handle = None; - Ok(()) - } - } else { - // There's no handle, so nothing to terminate. - Ok(()) - } - } - - pub async fn status(&self) -> Result { - if let Some(handle) = &self.handle { - handle.status().await - } else { - Ok(Status::None) - } - } -} - pub struct StartOptions { pub ctx: tower_telemetry::Context, pub package: Package, From 3b2947c2182aa2d4a06565c03621ae141e1c9adf Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Wed, 14 Jan 2026 11:53:24 +0100 Subject: [PATCH 13/21] minor --- crates/tower-runtime/src/backends/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tower-runtime/src/backends/cli.rs b/crates/tower-runtime/src/backends/cli.rs index 537bc589..e37327af 100644 --- a/crates/tower-runtime/src/backends/cli.rs +++ b/crates/tower-runtime/src/backends/cli.rs @@ -4,7 +4,7 @@ //! - The caller creates the output channel and passes the sender //! - The caller keeps the receiver for direct consumption //! - No need for complex handle.logs() method -//! - Single consumer model (perfect for CLI) +//! - Single consumer model (better for CLI) use crate::errors::Error; use crate::local::LocalApp; From 6d8860d131b34caa50091c5152d7b8bc6289ce2f Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Wed, 14 Jan 2026 12:08:37 +0100 Subject: [PATCH 14/21] Update deps --- Cargo.lock | 24 ++++++++++++++++-------- Cargo.toml | 3 +-- crates/tower-runtime/Cargo.toml | 1 - 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 365da75c..d3a0df64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3348,10 +3348,11 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] @@ -3365,11 +3366,20 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -4223,7 +4233,6 @@ dependencies = [ "tower-package", "tower-telemetry", "tower-uv", - "uuid", ] [[package]] @@ -4476,13 +4485,12 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.3", "js-sys", - "serde", "wasm-bindgen", ] diff --git a/Cargo.toml b/Cargo.toml index af88974e..3a01da08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ repository = "https://github.com/tower/tower-cli" aes-gcm = "0.10" anyhow = "1.0.95" async-compression = { version = "0.4", features = ["tokio", "gzip"] } -async-trait = "0.1.83" +async-trait = "0.1.89" async_zip = { version = "0.0.16", features = ["tokio", "tokio-fs", "deflate"] } axum = "0.8.4" base64 = "0.22" @@ -66,7 +66,6 @@ tracing = { version = "0.1" } tracing-appender = "0.2" tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] } url = { version = "2", features = ["serde"] } -uuid = { version = "1.11", features = ["v4", "serde"] } webbrowser = "1" # The profile that 'dist' will build with diff --git a/crates/tower-runtime/Cargo.toml b/crates/tower-runtime/Cargo.toml index 3ed51369..168dea14 100644 --- a/crates/tower-runtime/Cargo.toml +++ b/crates/tower-runtime/Cargo.toml @@ -16,7 +16,6 @@ tokio-util = { workspace = true } tower-package = { workspace = true } tower-telemetry = { workspace = true } tower-uv = { workspace = true } -uuid = { workspace = true } # K8s dependencies (optional) k8s-openapi = { version = "0.23", features = ["v1_31"], optional = true } From bf8b0425efd548a5c7e3da8d81c9ae2b0d8ac713 Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Wed, 14 Jan 2026 13:29:58 +0100 Subject: [PATCH 15/21] dep updates --- Cargo.lock | 386 ++++++++---------- Cargo.toml | 2 + crates/tower-runtime/Cargo.toml | 8 +- crates/tower-runtime/src/backends/mod.rs | 1 - .../test-home/.config/tower/session.json | 29 +- 5 files changed, 210 insertions(+), 216 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d3a0df64..8ed353a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -286,14 +286,14 @@ dependencies = [ ] [[package]] -name = "backoff" -version = "0.4.0" +name = "backon" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" dependencies = [ - "getrandom 0.2.16", - "instant", - "rand 0.8.5", + "fastrand", + "gloo-timers", + "tokio", ] [[package]] @@ -579,16 +579,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[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" @@ -733,7 +723,7 @@ dependencies = [ "console", "cucumber-codegen", "cucumber-expressions", - "derive_more", + "derive_more 0.99.20", "drain_filter_polyfill", "either", "futures", @@ -773,7 +763,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d794fed319eea24246fb5f57632f7ae38d61195817b7eb659455aa5bdd7c1810" dependencies = [ - "derive_more", + "derive_more 0.99.20", "either", "nom", "nom_locate", @@ -801,6 +791,16 @@ dependencies = [ "darling_macro 0.21.2", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + [[package]] name = "darling_core" version = "0.20.11" @@ -829,6 +829,19 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.104", +] + [[package]] name = "darling_macro" version = "0.20.11" @@ -851,6 +864,17 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.104", +] + [[package]] name = "der" version = "0.7.10" @@ -883,6 +907,27 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.104", +] + [[package]] name = "digest" version = "0.10.7" @@ -1112,21 +1157,18 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "fluent-uri" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[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.1" @@ -1345,20 +1387,22 @@ dependencies = [ ] [[package]] -name = "hashbrown" -version = "0.12.3" +name = "gloo-timers" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" @@ -1367,27 +1411,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" [[package]] -name = "headers" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" -dependencies = [ - "base64", - "bytes", - "headers-core", - "http", - "httpdate", - "mime", - "sha1", -] - -[[package]] -name = "headers-core" -version = "0.3.0" +name = "hashbrown" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "http", + "allocator-api2", + "equivalent", + "foldhash", ] [[package]] @@ -1409,12 +1440,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "home" -version = "0.5.12" +name = "hostname" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" dependencies = [ - "windows-sys 0.61.2", + "cfg-if", + "libc", + "windows-link 0.2.1", ] [[package]] @@ -1489,26 +1522,6 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-http-proxy" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ad4b0a1e37510028bc4ba81d0e38d239c39671b0f0ce9e02dfa93a8133f7c08" -dependencies = [ - "bytes", - "futures-util", - "headers", - "http", - "hyper", - "hyper-rustls", - "hyper-util", - "pin-project-lite", - "rustls-native-certs 0.7.3", - "tokio", - "tokio-rustls", - "tower-service", -] - [[package]] name = "hyper-rustls" version = "0.27.7" @@ -1520,7 +1533,7 @@ dependencies = [ "hyper-util", "log", "rustls", - "rustls-native-certs 0.8.3", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", @@ -1768,15 +1781,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "inventory" version = "0.3.21" @@ -1834,6 +1838,30 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jiff" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "jni" version = "0.21.1" @@ -1868,9 +1896,9 @@ dependencies = [ [[package]] name = "json-patch" -version = "2.0.0" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1fb8864823fad91877e6caea0baca82e49e8db50f8e5c9f9a453e27d3330fc" +checksum = "f300e415e2134745ef75f04562dd0145405c2f7fd92065db029ac4b16b57fe90" dependencies = [ "jsonptr", "serde", @@ -1880,48 +1908,44 @@ dependencies = [ [[package]] name = "jsonpath-rust" -version = "0.5.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d8fe85bd70ff715f31ce8c739194b423d79811a19602115d611a3ec85d6200" +checksum = "633a7320c4bb672863a3782e89b9094ad70285e097ff6832cddd0ec615beadfa" dependencies = [ - "lazy_static", - "once_cell", "pest", "pest_derive", "regex", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] name = "jsonptr" -version = "0.4.7" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c6e529149475ca0b2820835d3dce8fcc41c6b943ca608d32f35b449255e4627" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" dependencies = [ - "fluent-uri", "serde", "serde_json", ] [[package]] name = "k8s-openapi" -version = "0.23.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8847402328d8301354c94d605481f25a6bdc1ed65471fd96af8eca71141b13" +checksum = "05a6d6f3611ad1d21732adbd7a2e921f598af6c92d71ae6e2620da4b67ee1f0d" dependencies = [ "base64", - "chrono", + "jiff", "serde", - "serde-value", "serde_json", ] [[package]] name = "kube" -version = "0.96.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efffeb3df0bd4ef3e5d65044573499c0e4889b988070b08c50b25b1329289a1f" +checksum = "0dae7229247e4215781e5c5104a056e1e2163943e577f9084cf8bba7b5248f7a" dependencies = [ "k8s-openapi", "kube-client", @@ -1932,35 +1956,32 @@ dependencies = [ [[package]] name = "kube-client" -version = "0.96.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf471ece8ff8d24735ce78dac4d091e9fcb8d74811aeb6b75de4d1c3f5de0f1" +checksum = "010875e291a9c0a4e076f4f9c35b97d82fd2372cb3bc713252c3d08b7e73ce5b" dependencies = [ "base64", "bytes", - "chrono", "either", "futures", - "home", "http", "http-body", "http-body-util", "hyper", - "hyper-http-proxy", "hyper-rustls", "hyper-timeout", "hyper-util", + "jiff", "jsonpath-rust", "k8s-openapi", "kube-core", "pem", "rustls", - "rustls-pemfile", "secrecy", "serde", "serde_json", "serde_yaml", - "thiserror 1.0.69", + "thiserror 2.0.12", "tokio", "tokio-util", "tower 0.5.2", @@ -1970,58 +1991,59 @@ dependencies = [ [[package]] name = "kube-core" -version = "0.96.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f42346d30bb34d1d7adc5c549b691bce7aa3a1e60254e68fab7e2d7b26fe3d77" +checksum = "1ac76281aa698dd34111e25b21f5f6561932a30feabab5357152be273f8a81bb" dependencies = [ - "chrono", + "derive_more 2.1.1", "form_urlencoded", "http", + "jiff", "json-patch", "k8s-openapi", - "schemars 0.8.22", + "schemars 1.0.4", "serde", "serde-value", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] name = "kube-derive" -version = "0.96.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9364e04cc5e0482136c6ee8b7fb7551812da25802249f35b3def7aaa31e82ad" +checksum = "599c09721efcccc0e6a26e93df28c587da60ff5e099c657626fff2af0ae4cbb8" dependencies = [ - "darling 0.20.11", + "darling 0.23.0", "proc-macro2", "quote", + "serde", "serde_json", "syn 2.0.104", ] [[package]] name = "kube-runtime" -version = "0.96.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fbf1f6ffa98e65f1d2a9a69338bb60605d46be7edf00237784b89e62c9bd44" +checksum = "6db43d26700f564baf850f681f3cb0f1195d2699bd379bfa70750ecec4dcb209" dependencies = [ "ahash", "async-broadcast", "async-stream", - "async-trait", - "backoff", + "backon", "educe", "futures", - "hashbrown 0.14.5", + "hashbrown 0.16.1", + "hostname", "json-patch", - "jsonptr", "k8s-openapi", "kube-client", "parking_lot", "pin-project", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.12", "tokio", "tokio-util", "tracing", @@ -2388,12 +2410,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - [[package]] name = "openssl-probe" version = "0.2.0" @@ -2622,6 +2638,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.2" @@ -3071,6 +3096,15 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[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 = "0.38.44" @@ -3112,38 +3146,16 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" -dependencies = [ - "openssl-probe 0.1.6", - "rustls-pemfile", - "rustls-pki-types", - "schannel", - "security-framework 2.11.1", -] - [[package]] name = "rustls-native-certs" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe 0.2.0", + "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.5.1", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", + "security-framework", ] [[package]] @@ -3221,18 +3233,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "schemars" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" -dependencies = [ - "dyn-clone", - "schemars_derive 0.8.22", - "serde", - "serde_json", -] - [[package]] name = "schemars" version = "0.9.0" @@ -3254,23 +3254,11 @@ dependencies = [ "chrono", "dyn-clone", "ref-cast", - "schemars_derive 1.0.4", + "schemars_derive", "serde", "serde_json", ] -[[package]] -name = "schemars_derive" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.104", -] - [[package]] name = "schemars_derive" version = "1.0.4" @@ -3310,19 +3298,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.9.1", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - [[package]] name = "security-framework" version = "3.5.1" @@ -3330,7 +3305,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ "bitflags 2.9.1", - "core-foundation 0.10.1", + "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", @@ -3346,6 +3321,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -3496,17 +3477,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sha2" version = "0.10.9" @@ -4650,7 +4620,7 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aaf4f3c0ba838e82b4e5ccc4157003fb8c324ee24c058470ffb82820becbde98" dependencies = [ - "core-foundation 0.10.1", + "core-foundation", "jni", "log", "ndk-context", diff --git a/Cargo.toml b/Cargo.toml index 3a01da08..2ef201ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,8 @@ tracing-appender = "0.2" tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] } url = { version = "2", features = ["serde"] } webbrowser = "1" +k8s-openapi = { version = "0.27.0", features = ["v1_31"] } +kube = { version = "3.0.0", features = ["runtime", "derive", "client", "rustls-tls"], default-features = false } # The profile that 'dist' will build with [profile.dist] diff --git a/crates/tower-runtime/Cargo.toml b/crates/tower-runtime/Cargo.toml index 168dea14..35c6131f 100644 --- a/crates/tower-runtime/Cargo.toml +++ b/crates/tower-runtime/Cargo.toml @@ -18,12 +18,8 @@ tower-telemetry = { workspace = true } tower-uv = { workspace = true } # K8s dependencies (optional) -k8s-openapi = { version = "0.23", features = ["v1_31"], optional = true } -kube = { version = "0.96", features = ["runtime", "derive", "client", "rustls-tls"], default-features = false, optional = true } - -[features] -default = [] -k8s = ["dep:k8s-openapi", "dep:kube"] +k8s-openapi = { workspace = true } +kube = { workspace = true } [dev-dependencies] config = { workspace = true } diff --git a/crates/tower-runtime/src/backends/mod.rs b/crates/tower-runtime/src/backends/mod.rs index b5cc4e3b..7d14ca14 100644 --- a/crates/tower-runtime/src/backends/mod.rs +++ b/crates/tower-runtime/src/backends/mod.rs @@ -2,7 +2,6 @@ pub mod subprocess; -#[cfg(feature = "k8s")] pub mod k8s; /// Simple backend for CLI --local runs diff --git a/tests/integration/test-home/.config/tower/session.json b/tests/integration/test-home/.config/tower/session.json index 469218e4..b3fc756b 100644 --- a/tests/integration/test-home/.config/tower/session.json +++ b/tests/integration/test-home/.config/tower/session.json @@ -1 +1,28 @@ -{"tower_url":"http://127.0.0.1:8000/v1","user":{"email":"test@example.com","first_name":"Test","last_name":"User","created_at":"2023-01-01T00:00:00Z"},"token":{"jwt":"mock_jwt_token"},"active_team":{"name":"default","token":{"jwt":"mock_jwt_token"},"team_type":"user"},"teams":[{"name":"default","token":{"jwt":"mock_jwt_token"},"team_type":"user"}]} \ No newline at end of file +{ + "tower_url": "http://127.0.0.1:8000/v1", + "user": { + "email": "test@example.com", + "first_name": "Test", + "last_name": "User", + "created_at": "2023-01-01T00:00:00Z" + }, + "token": { + "jwt": "mock_jwt_token" + }, + "active_team": { + "name": "default", + "token": { + "jwt": "mock_jwt_token" + }, + "team_type": "user" + }, + "teams": [ + { + "name": "default", + "token": { + "jwt": "mock_jwt_token" + }, + "team_type": "user" + } + ] +} \ No newline at end of file From ad2c197f65a7e7be21cd8247ed1c32d0e09acca6 Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Wed, 14 Jan 2026 15:29:04 +0100 Subject: [PATCH 16/21] BundleRef -> PackageRef --- crates/tower-runtime/src/backends/k8s.rs | 8 ++++---- crates/tower-runtime/src/backends/subprocess.rs | 8 ++++---- crates/tower-runtime/src/execution.rs | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/tower-runtime/src/backends/k8s.rs b/crates/tower-runtime/src/backends/k8s.rs index 9b54e6af..6a94377d 100644 --- a/crates/tower-runtime/src/backends/k8s.rs +++ b/crates/tower-runtime/src/backends/k8s.rs @@ -10,8 +10,8 @@ use crate::errors::Error; use crate::execution::{ - BackendCapabilities, BundleRef, ExecutionBackend, ExecutionHandle, ExecutionSpec, - NetworkingSpec, ServiceEndpoint, + BackendCapabilities, ExecutionBackend, ExecutionHandle, ExecutionSpec, NetworkingSpec, + PackageRef, ServiceEndpoint, }; use crate::{Channel, Output, OutputReceiver, Status, FD}; @@ -220,8 +220,8 @@ impl K8sBackend { let configmaps: Api = Api::namespaced(self.client.clone(), &self.namespace); // Get bundle path - let bundle_path = match &spec.bundle { - BundleRef::Local { path } => path, + let bundle_path = match &spec.package { + PackageRef::Local { path } => path, }; // Recursively read ALL files from the bundle directory diff --git a/crates/tower-runtime/src/backends/subprocess.rs b/crates/tower-runtime/src/backends/subprocess.rs index b2d65806..57e99e41 100644 --- a/crates/tower-runtime/src/backends/subprocess.rs +++ b/crates/tower-runtime/src/backends/subprocess.rs @@ -2,8 +2,8 @@ use crate::errors::Error; use crate::execution::{ - BackendCapabilities, BundleRef, CacheBackend, ExecutionBackend, ExecutionHandle, ExecutionSpec, - ServiceEndpoint, + BackendCapabilities, CacheBackend, ExecutionBackend, ExecutionHandle, ExecutionSpec, + PackageRef, ServiceEndpoint, }; use crate::local::LocalApp; use crate::{App, OutputReceiver, StartOptions, Status}; @@ -43,8 +43,8 @@ impl ExecutionBackend for SubprocessBackend { let opts = StartOptions { ctx: spec.telemetry_ctx, - package: match spec.bundle { - BundleRef::Local { path } => Package::from_unpacked_path(path).await, + package: match spec.package { + PackageRef::Local { path } => Package::from_unpacked_path(path).await, }, cwd: None, // LocalApp determines cwd from package environment: spec.environment, diff --git a/crates/tower-runtime/src/execution.rs b/crates/tower-runtime/src/execution.rs index 76bb0481..455e4a21 100644 --- a/crates/tower-runtime/src/execution.rs +++ b/crates/tower-runtime/src/execution.rs @@ -21,8 +21,8 @@ pub struct ExecutionSpec { /// Unique identifier for this execution (e.g., run_id) pub id: String, - /// Bundle reference (how to get the application code) - pub bundle: BundleRef, + /// Package reference (how to get the application code) + pub package: PackageRef, /// Runtime configuration (image, version, etc.) pub runtime: RuntimeConfig, @@ -49,9 +49,9 @@ pub struct ExecutionSpec { pub telemetry_ctx: tower_telemetry::Context, } -/// BundleRef describes where to get the application bundle +/// PackageRef describes where to get the application bundle #[derive(Debug, Clone)] -pub enum BundleRef { +pub enum PackageRef { /// Local filesystem path Local { path: PathBuf }, } From 87c1fbef49a5d56c215fb400dfabc1a14c31607e Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Thu, 15 Jan 2026 11:33:07 +0100 Subject: [PATCH 17/21] minor --- crates/tower-cmd/src/run.rs | 3 +-- crates/tower-runtime/src/execution.rs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/tower-cmd/src/run.rs b/crates/tower-cmd/src/run.rs index 55b33e02..bcb5f5fe 100644 --- a/crates/tower-cmd/src/run.rs +++ b/crates/tower-cmd/src/run.rs @@ -8,6 +8,7 @@ use tower_package::{Package, PackageSpec}; use tower_runtime::{OutputReceiver, Status}; use tower_telemetry::{debug, Context}; +use crate::{api, output, util::dates}; use std::sync::Arc; use tokio::sync::{ mpsc::unbounded_channel, @@ -17,8 +18,6 @@ use tokio::sync::{ }; use tokio::time::{sleep, timeout, Duration}; -use crate::{api, output, util::dates}; - pub fn run_cmd() -> Command { Command::new("run") .allow_external_subcommands(true) diff --git a/crates/tower-runtime/src/execution.rs b/crates/tower-runtime/src/execution.rs index 455e4a21..5e901244 100644 --- a/crates/tower-runtime/src/execution.rs +++ b/crates/tower-runtime/src/execution.rs @@ -208,7 +208,7 @@ pub struct BackendCapabilities { /// ExecutionHandle represents a running execution #[async_trait] pub trait ExecutionHandle: Send + Sync { - /// Get unique identifier for this execution + /// Get a unique identifier for this execution fn id(&self) -> &str; /// Get current execution status From 10860275741198f3c21d7ce3423824bb7b683d6d Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Thu, 15 Jan 2026 11:49:14 +0100 Subject: [PATCH 18/21] Remove cli backend --- crates/tower-cmd/src/run.rs | 90 ++++++++++++++----- crates/tower-runtime/src/backends/cli.rs | 90 ------------------- crates/tower-runtime/src/backends/mod.rs | 3 - .../test-home/.config/tower/session.json | 29 +----- 4 files changed, 71 insertions(+), 141 deletions(-) delete mode 100644 crates/tower-runtime/src/backends/cli.rs diff --git a/crates/tower-cmd/src/run.rs b/crates/tower-cmd/src/run.rs index bcb5f5fe..4dd4e634 100644 --- a/crates/tower-cmd/src/run.rs +++ b/crates/tower-cmd/src/run.rs @@ -11,7 +11,6 @@ use tower_telemetry::{debug, Context}; use crate::{api, output, util::dates}; use std::sync::Arc; use tokio::sync::{ - mpsc::unbounded_channel, mpsc::Receiver as MpscReceiver, oneshot::{self, Receiver as OneshotReceiver}, Mutex, @@ -168,26 +167,73 @@ where // Unpack the package package.unpack().await?; - // Create output channel - simple pattern for CLI - let (sender, receiver) = unbounded_channel(); - output::success(&format!("Launching app `{}`", towerfile.app.name)); - let output_task = tokio::spawn(output_handler(receiver)); - // Create backend and launch app - use tower_runtime::backends::cli::CliBackend; - let backend = CliBackend::new(config.cache_dir); - let handle = backend - .launch( - Context::new(), - sender, - package, - env.to_string(), - secrets, - params, - env_vars, - ) - .await?; + // Create backend and launch app using SubprocessBackend + use tower_runtime::backends::subprocess::SubprocessBackend; + use tower_runtime::execution::{ + CacheBackend, CacheConfig, CacheIsolation, ExecutionBackend, ExecutionSpec, PackageRef, + ResourceLimits, RuntimeConfig as ExecRuntimeConfig, + }; + + let backend = SubprocessBackend::new(config.cache_dir.clone()); + + // Build ExecutionSpec for SubprocessBackend + use std::time::{SystemTime, UNIX_EPOCH}; + let run_id = format!( + "cli-run-{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + ); + + let spec = ExecutionSpec { + id: run_id, + package: PackageRef::Local { + path: package + .unpacked_path + .clone() + .expect("Package must be unpacked before execution"), + }, + runtime: ExecRuntimeConfig { + image: "local".to_string(), + version: None, + cache: CacheConfig { + enable_bundle_cache: true, + enable_runtime_cache: true, + enable_dependency_cache: true, + backend: match config.cache_dir.clone() { + Some(dir) => CacheBackend::Local { cache_dir: dir }, + None => CacheBackend::None, + }, + isolation: CacheIsolation::None, + }, + entrypoint: None, + command: None, + }, + environment: env.to_string(), + secrets, + parameters: params, + env_vars, + resources: ResourceLimits { + cpu_millicores: None, + memory_mb: None, + storage_mb: None, + max_pids: None, + gpu_count: 0, + timeout_seconds: 3600, + }, + networking: None, + telemetry_ctx: Context::new(), + }; + + let handle = backend.create(spec).await?; + + // Get log receiver from handle + use tower_runtime::execution::ExecutionHandle as _; + let receiver = handle.logs().await?; + let output_task = tokio::spawn(output_handler(receiver)); // Monitor app status concurrently let handle = Arc::new(Mutex::new(handle)); @@ -597,7 +643,11 @@ async fn monitor_output(mut output: OutputReceiver) { /// monitor_local_status is a helper function that will monitor the status of a given app and waits for /// it to progress to a terminal state. -async fn monitor_cli_status(handle: Arc>) -> Status { +async fn monitor_cli_status( + handle: Arc>, +) -> Status { + use tower_runtime::execution::ExecutionHandle as _; + debug!("Starting status monitoring for CLI execution"); let mut check_count = 0; let mut err_count = 0; diff --git a/crates/tower-runtime/src/backends/cli.rs b/crates/tower-runtime/src/backends/cli.rs deleted file mode 100644 index e37327af..00000000 --- a/crates/tower-runtime/src/backends/cli.rs +++ /dev/null @@ -1,90 +0,0 @@ -//! Simple backend for CLI --local runs -//! -//! This backend follows a simpler pattern than SubprocessBackend: -//! - The caller creates the output channel and passes the sender -//! - The caller keeps the receiver for direct consumption -//! - No need for complex handle.logs() method -//! - Single consumer model (better for CLI) - -use crate::errors::Error; -use crate::local::LocalApp; -use crate::{App, OutputSender, StartOptions, Status}; -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::Arc; -use tokio::sync::Mutex; -use tower_package::Package; - -/// CliBackend executes apps as local subprocesses for CLI --local runs -pub struct CliBackend { - /// Optional default cache directory to use - cache_dir: Option, -} - -impl CliBackend { - pub fn new(cache_dir: Option) -> Self { - Self { cache_dir } - } - - /// Launch an app with the given parameters - /// - /// Unlike SubprocessBackend.create(), this takes OutputSender directly - /// so the caller can immediately start consuming output from their receiver. - pub async fn launch( - &self, - ctx: tower_telemetry::Context, - output_sender: OutputSender, - package: Package, - environment: String, - secrets: HashMap, - parameters: HashMap, - env_vars: HashMap, - ) -> Result { - let opts = StartOptions { - ctx, - package, - cwd: None, // LocalApp determines cwd from package - environment, - secrets, - parameters, - env_vars, - output_sender, - cache_dir: self.cache_dir.clone(), - }; - - // Start the LocalApp - let app = LocalApp::start(opts).await?; - - Ok(CliHandle { - app: Arc::new(Mutex::new(Some(app))), - }) - } -} - -/// CliHandle provides lifecycle management for a CLI local subprocess execution -pub struct CliHandle { - app: Arc>>, -} - -impl CliHandle { - /// Get current execution status - pub async fn status(&self) -> Result { - let guard = self.app.lock().await; - if let Some(app) = guard.as_ref() { - app.status().await - } else { - // App has already been terminated - Ok(Status::Crashed { code: -1 }) - } - } - - /// Terminate execution gracefully - pub async fn terminate(&mut self) -> Result<(), Error> { - let mut guard = self.app.lock().await; - if let Some(mut app) = guard.take() { - app.terminate().await - } else { - Ok(()) - } - } -} diff --git a/crates/tower-runtime/src/backends/mod.rs b/crates/tower-runtime/src/backends/mod.rs index 7d14ca14..7630f4cf 100644 --- a/crates/tower-runtime/src/backends/mod.rs +++ b/crates/tower-runtime/src/backends/mod.rs @@ -3,6 +3,3 @@ pub mod subprocess; pub mod k8s; - -/// Simple backend for CLI --local runs -pub mod cli; diff --git a/tests/integration/test-home/.config/tower/session.json b/tests/integration/test-home/.config/tower/session.json index b3fc756b..469218e4 100644 --- a/tests/integration/test-home/.config/tower/session.json +++ b/tests/integration/test-home/.config/tower/session.json @@ -1,28 +1 @@ -{ - "tower_url": "http://127.0.0.1:8000/v1", - "user": { - "email": "test@example.com", - "first_name": "Test", - "last_name": "User", - "created_at": "2023-01-01T00:00:00Z" - }, - "token": { - "jwt": "mock_jwt_token" - }, - "active_team": { - "name": "default", - "token": { - "jwt": "mock_jwt_token" - }, - "team_type": "user" - }, - "teams": [ - { - "name": "default", - "token": { - "jwt": "mock_jwt_token" - }, - "team_type": "user" - } - ] -} \ No newline at end of file +{"tower_url":"http://127.0.0.1:8000/v1","user":{"email":"test@example.com","first_name":"Test","last_name":"User","created_at":"2023-01-01T00:00:00Z"},"token":{"jwt":"mock_jwt_token"},"active_team":{"name":"default","token":{"jwt":"mock_jwt_token"},"team_type":"user"},"teams":[{"name":"default","token":{"jwt":"mock_jwt_token"},"team_type":"user"}]} \ No newline at end of file From 3ac4ab19600cbb9d66f0cf75c23bcbe639b79f24 Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Thu, 15 Jan 2026 12:05:02 +0100 Subject: [PATCH 19/21] Move out k8s backend --- Cargo.lock | 598 +---------------- Cargo.toml | 2 - crates/tower-cmd/src/run.rs | 4 +- crates/tower-runtime/Cargo.toml | 4 - crates/tower-runtime/src/backends/k8s.rs | 607 ------------------ crates/tower-runtime/src/backends/mod.rs | 5 - crates/tower-runtime/src/lib.rs | 2 +- .../src/{backends => }/subprocess.rs | 0 8 files changed, 9 insertions(+), 1213 deletions(-) delete mode 100644 crates/tower-runtime/src/backends/k8s.rs delete mode 100644 crates/tower-runtime/src/backends/mod.rs rename crates/tower-runtime/src/{backends => }/subprocess.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 8ed353a1..4201a821 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,19 +52,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.3", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -74,12 +61,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "android-tzdata" version = "0.1.1" @@ -151,18 +132,6 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" -[[package]] -name = "async-broadcast" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" -dependencies = [ - "event-listener", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - [[package]] name = "async-compression" version = "0.4.27" @@ -285,17 +254,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "backon" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" -dependencies = [ - "fastrand", - "gloo-timers", - "tokio", -] - [[package]] name = "backtrace" version = "0.3.75" @@ -417,7 +375,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -531,15 +489,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "config" version = "0.3.39" @@ -723,7 +672,7 @@ dependencies = [ "console", "cucumber-codegen", "cucumber-expressions", - "derive_more 0.99.20", + "derive_more", "drain_filter_polyfill", "either", "futures", @@ -763,7 +712,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d794fed319eea24246fb5f57632f7ae38d61195817b7eb659455aa5bdd7c1810" dependencies = [ - "derive_more 0.99.20", + "derive_more", "either", "nom", "nom_locate", @@ -791,16 +740,6 @@ dependencies = [ "darling_macro 0.21.2", ] -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - [[package]] name = "darling_core" version = "0.20.11" @@ -829,19 +768,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.104", -] - [[package]] name = "darling_macro" version = "0.20.11" @@ -864,17 +790,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core 0.23.0", - "quote", - "syn 2.0.104", -] - [[package]] name = "der" version = "0.7.10" @@ -907,27 +822,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.104", -] - [[package]] name = "digest" version = "0.10.7" @@ -1010,18 +904,6 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" -[[package]] -name = "educe" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" -dependencies = [ - "enum-ordinalize", - "proc-macro2", - "quote", - "syn 2.0.104", -] - [[package]] name = "either" version = "1.15.0" @@ -1040,26 +922,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" -[[package]] -name = "enum-ordinalize" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" -dependencies = [ - "enum-ordinalize-derive", -] - -[[package]] -name = "enum-ordinalize-derive" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -1086,27 +948,6 @@ dependencies = [ "str-buf", ] -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "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 = "eventsource-stream" version = "0.2.3" @@ -1163,12 +1004,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[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.1" @@ -1386,18 +1221,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "hashbrown" version = "0.12.3" @@ -1410,17 +1233,6 @@ version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] - [[package]] name = "heck" version = "0.4.1" @@ -1439,17 +1251,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hostname" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" -dependencies = [ - "cfg-if", - "libc", - "windows-link 0.2.1", -] - [[package]] name = "http" version = "1.3.1" @@ -1531,9 +1332,7 @@ dependencies = [ "http", "hyper", "hyper-util", - "log", "rustls", - "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", @@ -1541,19 +1340,6 @@ dependencies = [ "webpki-roots", ] -[[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.16" @@ -1838,30 +1624,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" -[[package]] -name = "jiff" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", -] - -[[package]] -name = "jiff-static" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - [[package]] name = "jni" version = "0.21.1" @@ -1894,161 +1656,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json-patch" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f300e415e2134745ef75f04562dd0145405c2f7fd92065db029ac4b16b57fe90" -dependencies = [ - "jsonptr", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "jsonpath-rust" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633a7320c4bb672863a3782e89b9094ad70285e097ff6832cddd0ec615beadfa" -dependencies = [ - "pest", - "pest_derive", - "regex", - "serde_json", - "thiserror 2.0.12", -] - -[[package]] -name = "jsonptr" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "k8s-openapi" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05a6d6f3611ad1d21732adbd7a2e921f598af6c92d71ae6e2620da4b67ee1f0d" -dependencies = [ - "base64", - "jiff", - "serde", - "serde_json", -] - -[[package]] -name = "kube" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dae7229247e4215781e5c5104a056e1e2163943e577f9084cf8bba7b5248f7a" -dependencies = [ - "k8s-openapi", - "kube-client", - "kube-core", - "kube-derive", - "kube-runtime", -] - -[[package]] -name = "kube-client" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "010875e291a9c0a4e076f4f9c35b97d82fd2372cb3bc713252c3d08b7e73ce5b" -dependencies = [ - "base64", - "bytes", - "either", - "futures", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-timeout", - "hyper-util", - "jiff", - "jsonpath-rust", - "k8s-openapi", - "kube-core", - "pem", - "rustls", - "secrecy", - "serde", - "serde_json", - "serde_yaml", - "thiserror 2.0.12", - "tokio", - "tokio-util", - "tower 0.5.2", - "tower-http", - "tracing", -] - -[[package]] -name = "kube-core" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac76281aa698dd34111e25b21f5f6561932a30feabab5357152be273f8a81bb" -dependencies = [ - "derive_more 2.1.1", - "form_urlencoded", - "http", - "jiff", - "json-patch", - "k8s-openapi", - "schemars 1.0.4", - "serde", - "serde-value", - "serde_json", - "thiserror 2.0.12", -] - -[[package]] -name = "kube-derive" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "599c09721efcccc0e6a26e93df28c587da60ff5e099c657626fff2af0ae4cbb8" -dependencies = [ - "darling 0.23.0", - "proc-macro2", - "quote", - "serde", - "serde_json", - "syn 2.0.104", -] - -[[package]] -name = "kube-runtime" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db43d26700f564baf850f681f3cb0f1195d2699bd379bfa70750ecec4dcb209" -dependencies = [ - "ahash", - "async-broadcast", - "async-stream", - "backon", - "educe", - "futures", - "hashbrown 0.16.1", - "hostname", - "json-patch", - "k8s-openapi", - "kube-client", - "parking_lot", - "pin-project", - "serde", - "serde_json", - "thiserror 2.0.12", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "lazy-regex" version = "3.4.1" @@ -2410,27 +2017,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "openssl-probe" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" - [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "ordered-float" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] - [[package]] name = "overload" version = "0.1.1" @@ -2524,49 +2116,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "pest" -version = "2.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "pest_meta" -version = "2.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" -dependencies = [ - "pest", - "sha2", -] - [[package]] name = "pin-project" version = "1.1.10" @@ -2638,15 +2187,6 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" -[[package]] -name = "portable-atomic-util" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" -dependencies = [ - "portable-atomic", -] - [[package]] name = "potential_utf" version = "0.1.2" @@ -3096,15 +2636,6 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" -[[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 = "0.38.44" @@ -3137,7 +2668,6 @@ version = "0.23.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" dependencies = [ - "log", "once_cell", "ring", "rustls-pki-types", @@ -3146,18 +2676,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - [[package]] name = "rustls-pki-types" version = "1.12.0" @@ -3224,15 +2742,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "schemars" version = "0.9.0" @@ -3289,44 +2798,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "secrecy" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" -dependencies = [ - "zeroize", -] - -[[package]] -name = "security-framework" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" -dependencies = [ - "bitflags 2.9.1", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - [[package]] name = "serde" version = "1.0.228" @@ -3337,16 +2808,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float", - "serde", -] - [[package]] name = "serde_core" version = "1.0.228" @@ -3464,19 +2925,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap 2.10.0", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "sha2" version = "0.10.9" @@ -4012,7 +3460,6 @@ dependencies = [ "futures-io", "futures-sink", "pin-project-lite", - "slab", "tokio", ] @@ -4077,7 +3524,6 @@ dependencies = [ "pin-project-lite", "sync_wrapper", "tokio", - "tokio-util", "tower-layer", "tower-service", "tracing", @@ -4147,19 +3593,16 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "base64", "bitflags 2.9.1", "bytes", "futures-util", "http", "http-body", "iri-string", - "mime", "pin-project-lite", "tower 0.5.2", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -4194,8 +3637,6 @@ dependencies = [ "async-trait", "chrono", "config", - "k8s-openapi", - "kube", "nix 0.30.1", "snafu", "tokio", @@ -4365,12 +3806,6 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - [[package]] name = "unicase" version = "2.8.1" @@ -4417,12 +3852,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "untrusted" version = "0.9.0" @@ -4678,7 +4107,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.1.3", + "windows-link", "windows-result", "windows-strings", ] @@ -4711,19 +4140,13 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -4732,7 +4155,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -4780,15 +4203,6 @@ dependencies = [ "windows-targets 0.53.2", ] -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-targets" version = "0.42.2" diff --git a/Cargo.toml b/Cargo.toml index 2ef201ee..3a01da08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,8 +67,6 @@ tracing-appender = "0.2" tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] } url = { version = "2", features = ["serde"] } webbrowser = "1" -k8s-openapi = { version = "0.27.0", features = ["v1_31"] } -kube = { version = "3.0.0", features = ["runtime", "derive", "client", "rustls-tls"], default-features = false } # The profile that 'dist' will build with [profile.dist] diff --git a/crates/tower-cmd/src/run.rs b/crates/tower-cmd/src/run.rs index 4dd4e634..f423616b 100644 --- a/crates/tower-cmd/src/run.rs +++ b/crates/tower-cmd/src/run.rs @@ -170,11 +170,11 @@ where output::success(&format!("Launching app `{}`", towerfile.app.name)); // Create backend and launch app using SubprocessBackend - use tower_runtime::backends::subprocess::SubprocessBackend; use tower_runtime::execution::{ CacheBackend, CacheConfig, CacheIsolation, ExecutionBackend, ExecutionSpec, PackageRef, ResourceLimits, RuntimeConfig as ExecRuntimeConfig, }; + use tower_runtime::subprocess::SubprocessBackend; let backend = SubprocessBackend::new(config.cache_dir.clone()); @@ -644,7 +644,7 @@ async fn monitor_output(mut output: OutputReceiver) { /// monitor_local_status is a helper function that will monitor the status of a given app and waits for /// it to progress to a terminal state. async fn monitor_cli_status( - handle: Arc>, + handle: Arc>, ) -> Status { use tower_runtime::execution::ExecutionHandle as _; diff --git a/crates/tower-runtime/Cargo.toml b/crates/tower-runtime/Cargo.toml index 35c6131f..fbeb31bb 100644 --- a/crates/tower-runtime/Cargo.toml +++ b/crates/tower-runtime/Cargo.toml @@ -17,9 +17,5 @@ tower-package = { workspace = true } tower-telemetry = { workspace = true } tower-uv = { workspace = true } -# K8s dependencies (optional) -k8s-openapi = { workspace = true } -kube = { workspace = true } - [dev-dependencies] config = { workspace = true } diff --git a/crates/tower-runtime/src/backends/k8s.rs b/crates/tower-runtime/src/backends/k8s.rs deleted file mode 100644 index 6a94377d..00000000 --- a/crates/tower-runtime/src/backends/k8s.rs +++ /dev/null @@ -1,607 +0,0 @@ -//! Kubernetes backend for Tower execution -//! -//! This module provides ExecutionBackend implementation for Kubernetes, supporting: -//! - Pod-based isolation with resource limits -//! - PersistentVolumeClaim-based caching -//! - Service endpoints for long-running apps -//! - Log streaming from pods -//! -//! Only available with the "k8s" feature flag. - -use crate::errors::Error; -use crate::execution::{ - BackendCapabilities, ExecutionBackend, ExecutionHandle, ExecutionSpec, NetworkingSpec, - PackageRef, ServiceEndpoint, -}; -use crate::{Channel, Output, OutputReceiver, Status, FD}; - -use async_trait::async_trait; -use chrono::Utc; -use k8s_openapi::api::core::v1::{ - ConfigMap, Container, Pod, PodSpec, ResourceRequirements, Service, ServicePort, ServiceSpec, - Volume, VolumeMount, -}; -use kube::{ - api::{Api, DeleteParams, LogParams, PostParams}, - runtime::wait::await_condition, - Client, -}; -use std::collections::BTreeMap; -use std::sync::Arc; -use tokio::io::{AsyncBufReadExt, BufReader}; -use tokio::sync::Mutex; -use tokio_util::compat::FuturesAsyncReadCompatExt; - -/// K8sBackend executes apps as Kubernetes Pods -pub struct K8sBackend { - client: Client, - namespace: String, -} - -impl K8sBackend { - /// Create a new K8sBackend - pub async fn new(namespace: String) -> Result { - let client = Client::try_default().await.map_err(|e| { - eprintln!("Failed to create Kubernetes client: {}", e); - Error::RuntimeStartFailed - })?; - - Ok(Self { client, namespace }) - } - - /// Build pod spec from execution spec - fn build_pod_spec( - &self, - spec: &ExecutionSpec, - path_mapping: &BTreeMap, - ) -> Result { - let mut labels = BTreeMap::new(); - labels.insert("app".to_string(), "tower-app".to_string()); - labels.insert("execution-id".to_string(), spec.id.clone()); - - // Build environment variables - let mut env_vars = vec![]; - for (key, value) in &spec.secrets { - env_vars.push(k8s_openapi::api::core::v1::EnvVar { - name: key.clone(), - value: Some(value.clone()), - ..Default::default() - }); - } - for (key, value) in &spec.parameters { - env_vars.push(k8s_openapi::api::core::v1::EnvVar { - name: key.clone(), - value: Some(value.clone()), - ..Default::default() - }); - } - for (key, value) in &spec.env_vars { - env_vars.push(k8s_openapi::api::core::v1::EnvVar { - name: key.clone(), - value: Some(value.clone()), - ..Default::default() - }); - } - - // Build volume mounts - let mut volume_mounts = vec![]; - let mut volumes = vec![]; - - // Build resource requirements - let mut resource_limits = BTreeMap::new(); - let mut resource_requests = BTreeMap::new(); - - if let Some(cpu) = spec.resources.cpu_millicores { - let cpu_str = format!("{}m", cpu); - resource_limits.insert( - "cpu".to_string(), - k8s_openapi::apimachinery::pkg::api::resource::Quantity(cpu_str.clone()), - ); - resource_requests.insert( - "cpu".to_string(), - k8s_openapi::apimachinery::pkg::api::resource::Quantity(cpu_str), - ); - } - - if let Some(memory) = spec.resources.memory_mb { - let mem_str = format!("{}Mi", memory); - resource_limits.insert( - "memory".to_string(), - k8s_openapi::apimachinery::pkg::api::resource::Quantity(mem_str.clone()), - ); - resource_requests.insert( - "memory".to_string(), - k8s_openapi::apimachinery::pkg::api::resource::Quantity(mem_str), - ); - } - - if let Some(storage) = spec.resources.storage_mb { - let storage_str = format!("{}Mi", storage); - resource_limits.insert( - "ephemeral-storage".to_string(), - k8s_openapi::apimachinery::pkg::api::resource::Quantity(storage_str.clone()), - ); - resource_requests.insert( - "ephemeral-storage".to_string(), - k8s_openapi::apimachinery::pkg::api::resource::Quantity(storage_str), - ); - } - - let resources = ResourceRequirements { - limits: Some(resource_limits), - requests: Some(resource_requests), - ..Default::default() - }; - - // Add bundle volume mount - volume_mounts.push(VolumeMount { - name: "bundle".to_string(), - mount_path: "/app".to_string(), - read_only: Some(true), - ..Default::default() - }); - - // Build items array to map ConfigMap keys to their original paths - // e.g., "app__task.py" -> "app/task.py" - let items: Vec = path_mapping - .iter() - .map( - |(sanitized_key, original_path)| k8s_openapi::api::core::v1::KeyToPath { - key: sanitized_key.clone(), - path: original_path.clone(), - mode: Some(0o755), - }, - ) - .collect(); - - // Bundle will be provided as a ConfigMap (created separately) - volumes.push(Volume { - name: "bundle".to_string(), - config_map: Some(k8s_openapi::api::core::v1::ConfigMapVolumeSource { - name: format!("bundle-{}", spec.id), - default_mode: Some(0o755), - items: Some(items), - ..Default::default() - }), - ..Default::default() - }); - - // Build container spec - // Note: In K8s, 'command' = entrypoint, 'args' = command - let container = Container { - name: "app".to_string(), - image: Some(spec.runtime.image.clone()), - env: Some(env_vars), - command: spec.runtime.entrypoint.clone(), // K8s command = entrypoint - args: spec.runtime.command.clone(), // K8s args = command - volume_mounts: if volume_mounts.is_empty() { - None - } else { - Some(volume_mounts) - }, - resources: Some(resources), - working_dir: Some("/app".to_string()), - ..Default::default() - }; - - // Build pod spec - let pod_spec = PodSpec { - containers: vec![container], - volumes: if volumes.is_empty() { - None - } else { - Some(volumes) - }, - restart_policy: Some("Never".to_string()), - ..Default::default() - }; - - Ok(Pod { - metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta { - name: Some(format!("tower-run-{}", spec.id)), - namespace: Some(self.namespace.clone()), - labels: Some(labels), - ..Default::default() - }, - spec: Some(pod_spec), - ..Default::default() - }) - } - - /// Create ConfigMap with bundle contents - /// Returns a mapping of sanitized keys to original paths for volume mounting - async fn create_bundle_configmap( - &self, - spec: &ExecutionSpec, - ) -> Result, Error> { - use k8s_openapi::api::core::v1::ConfigMap; - use std::collections::BTreeMap; - - let configmaps: Api = Api::namespaced(self.client.clone(), &self.namespace); - - // Get bundle path - let bundle_path = match &spec.package { - PackageRef::Local { path } => path, - }; - - // Recursively read ALL files from the bundle directory - let mut data = BTreeMap::new(); - let mut binary_data = BTreeMap::new(); - let mut path_mapping = BTreeMap::new(); // sanitized_key -> original_path - - Self::walk_directory( - &bundle_path, - &bundle_path, - &mut data, - &mut binary_data, - &mut path_mapping, - ) - .await?; - - if data.is_empty() && binary_data.is_empty() { - return Err(Error::RuntimeStartFailed); // No files found - } - - let configmap = ConfigMap { - metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta { - name: Some(format!("bundle-{}", spec.id)), - namespace: Some(self.namespace.clone()), - ..Default::default() - }, - data: if !data.is_empty() { Some(data) } else { None }, - binary_data: if !binary_data.is_empty() { - Some(binary_data) - } else { - None - }, - ..Default::default() - }; - - configmaps - .create(&PostParams::default(), &configmap) - .await - .map_err(|_| Error::RuntimeStartFailed)?; - - Ok(path_mapping) - } - - /// Sanitize a file path to be a valid ConfigMap key - /// Replaces '/' with '__' to comply with K8s key restrictions: [-._a-zA-Z0-9]+ - fn sanitize_configmap_key(path: &str) -> String { - path.replace('/', "__") - } - - /// Recursively walk directory and collect all files - async fn walk_directory( - current_path: &std::path::Path, - base_path: &std::path::Path, - text_data: &mut BTreeMap, - binary_data: &mut BTreeMap, - path_mapping: &mut BTreeMap, - ) -> Result<(), Error> { - use tokio::fs; - - let mut entries = fs::read_dir(current_path) - .await - .map_err(|_| Error::RuntimeStartFailed)?; - - while let Some(entry) = entries - .next_entry() - .await - .map_err(|_| Error::RuntimeStartFailed)? - { - let path = entry.path(); - - if path.is_dir() { - // Recursively process subdirectories - Box::pin(Self::walk_directory( - &path, - base_path, - text_data, - binary_data, - path_mapping, - )) - .await?; - } else if path.is_file() { - // Get relative path from base (e.g., "app/task.py") - let relative_path = path - .strip_prefix(base_path) - .map_err(|_| Error::RuntimeStartFailed)? - .to_str() - .ok_or(Error::RuntimeStartFailed)? - .to_string(); - - // Sanitize the key for ConfigMap (e.g., "app/task.py" -> "app__task.py") - let sanitized_key = Self::sanitize_configmap_key(&relative_path); - - // Store mapping for volume mount reconstruction - path_mapping.insert(sanitized_key.clone(), relative_path.clone()); - - // Try reading as text first - match fs::read_to_string(&path).await { - Ok(contents) => { - text_data.insert(sanitized_key, contents); - } - Err(_) => { - // If not text, read as binary - if let Ok(contents) = fs::read(&path).await { - binary_data.insert(sanitized_key, k8s_openapi::ByteString(contents)); - } - } - } - } - } - - Ok(()) - } - - /// Build service spec for networking - fn build_service_spec( - &self, - exec_id: &str, - networking: &NetworkingSpec, - ) -> Result { - let mut labels = BTreeMap::new(); - labels.insert("app".to_string(), "tower-app".to_string()); - labels.insert("execution-id".to_string(), exec_id.to_string()); - - let service_port = ServicePort { - name: Some("http".to_string()), - port: networking.port as i32, - target_port: Some( - k8s_openapi::apimachinery::pkg::util::intstr::IntOrString::Int( - networking.port as i32, - ), - ), - ..Default::default() - }; - - let service_spec = ServiceSpec { - selector: Some(labels.clone()), - ports: Some(vec![service_port]), - type_: Some("ClusterIP".to_string()), - ..Default::default() - }; - - Ok(Service { - metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta { - name: Some( - networking - .service_name - .clone() - .unwrap_or_else(|| format!("tower-svc-{}", exec_id)), - ), - namespace: Some(self.namespace.clone()), - labels: Some(labels), - ..Default::default() - }, - spec: Some(service_spec), - ..Default::default() - }) - } -} - -#[async_trait] -impl ExecutionBackend for K8sBackend { - type Handle = K8sHandle; - - async fn create(&self, spec: ExecutionSpec) -> Result { - let pods: Api = Api::namespaced(self.client.clone(), &self.namespace); - - // Create ConfigMap with bundle contents and get path mapping - let path_mapping = self.create_bundle_configmap(&spec).await?; - - // Build and create pod with path mapping for volume items - let pod = self.build_pod_spec(&spec, &path_mapping)?; - let pod_name = pod.metadata.name.clone().ok_or(Error::RuntimeStartFailed)?; - - pods.create(&PostParams::default(), &pod) - .await - .map_err(|_| Error::RuntimeStartFailed)?; - - // Create service if networking is specified - let service_endpoint = if let Some(networking) = &spec.networking { - if networking.expose_service { - let services: Api = Api::namespaced(self.client.clone(), &self.namespace); - let service = self.build_service_spec(&spec.id, networking)?; - let service_name = service - .metadata - .name - .clone() - .ok_or(Error::RuntimeStartFailed)?; - - services - .create(&PostParams::default(), &service) - .await - .map_err(|_| Error::RuntimeStartFailed)?; - - Some(ServiceEndpoint { - host: format!("{}.{}.svc.cluster.local", service_name, self.namespace), - port: networking.port, - protocol: "http".to_string(), - url: Some(format!( - "http://{}.{}.svc.cluster.local:{}", - service_name, self.namespace, networking.port - )), - }) - } else { - None - } - } else { - None - }; - - Ok(K8sHandle { - id: spec.id, - pod_name, - namespace: self.namespace.clone(), - client: self.client.clone(), - service_endpoint: Arc::new(Mutex::new(service_endpoint)), - }) - } - - fn capabilities(&self) -> BackendCapabilities { - BackendCapabilities { - name: "k8s".to_string(), - supports_persistent_cache: true, - supports_prewarming: true, - supports_network_isolation: true, - supports_service_endpoints: true, - typical_cold_start_ms: 5000, // ~5s for image pull + pod start - typical_warm_start_ms: 1000, // ~1s with cached image - max_concurrent_executions: None, // Limited by cluster capacity - } - } - - async fn cleanup(&self) -> Result<(), Error> { - // No global cleanup needed for K8s backend - Ok(()) - } -} - -/// K8sHandle provides lifecycle management for a Kubernetes Pod execution -pub struct K8sHandle { - id: String, - pod_name: String, - namespace: String, - client: Client, - service_endpoint: Arc>>, -} - -#[async_trait] -impl ExecutionHandle for K8sHandle { - fn id(&self) -> &str { - &self.id - } - - async fn status(&self) -> Result { - let pods: Api = Api::namespaced(self.client.clone(), &self.namespace); - - let pod = pods - .get(&self.pod_name) - .await - .map_err(|_| Error::NoRunningApp)?; - - let phase = pod - .status - .and_then(|s| s.phase) - .unwrap_or_else(|| "Unknown".to_string()); - - Ok(match phase.as_str() { - "Pending" => Status::None, - "Running" => Status::Running, - "Succeeded" => Status::Exited, - "Failed" => Status::Crashed { code: 1 }, - _ => Status::None, - }) - } - - async fn logs(&self) -> Result { - let pods: Api = Api::namespaced(self.client.clone(), &self.namespace); - let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); - - let pod_name = self.pod_name.clone(); - let pods_clone = pods.clone(); - - tokio::spawn(async move { - // Wait for pod to have containers created (Running, Succeeded, or Failed) - // This ensures we can stream logs even if the pod crashes - let condition = await_condition(pods_clone.clone(), &pod_name, |obj: Option<&Pod>| { - obj.and_then(|pod| pod.status.as_ref()) - .and_then(|status| status.phase.as_ref()) - .map(|phase| phase == "Running" || phase == "Succeeded" || phase == "Failed") - .unwrap_or(false) - }); - - // Wait with a timeout - if tokio::time::timeout(std::time::Duration::from_secs(60), condition) - .await - .is_ok() - { - let log_params = LogParams { - follow: true, - ..Default::default() - }; - - if let Ok(logs) = pods_clone.log_stream(&pod_name, &log_params).await { - // Convert futures AsyncBufRead to tokio AsyncRead - let compat_logs = logs.compat(); - let mut reader = BufReader::new(compat_logs).lines(); - while let Ok(Some(line)) = reader.next_line().await { - let output = Output { - time: Utc::now(), - fd: FD::Stdout, // K8s combines stdout/stderr - channel: Channel::Program, - line, - }; - if tx.send(output).is_err() { - break; - } - } - } - } - }); - - Ok(rx) - } - - async fn terminate(&mut self) -> Result<(), Error> { - let pods: Api = Api::namespaced(self.client.clone(), &self.namespace); - - pods.delete(&self.pod_name, &DeleteParams::default()) - .await - .map_err(|_| Error::TerminateFailed)?; - - Ok(()) - } - - async fn kill(&mut self) -> Result<(), Error> { - // For K8s, kill is the same as terminate (pod deletion) - self.terminate().await - } - - async fn wait_for_completion(&self) -> Result { - let pods: Api = Api::namespaced(self.client.clone(), &self.namespace); - - // Wait for pod to reach terminal state - await_condition(pods.clone(), &self.pod_name, |obj: Option<&Pod>| { - obj.and_then(|pod| pod.status.as_ref()) - .and_then(|status| status.phase.as_ref()) - .map(|phase| phase == "Succeeded" || phase == "Failed") - .unwrap_or(false) - }) - .await - .map_err(|_| Error::Timeout)?; - - self.status().await - } - - async fn service_endpoint(&self) -> Result, Error> { - let endpoint = self.service_endpoint.lock().await; - Ok(endpoint.clone()) - } - - async fn cleanup(&mut self) -> Result<(), Error> { - // Delete pod - self.terminate().await?; - - // Delete ConfigMap with bundle - let configmaps: Api = Api::namespaced(self.client.clone(), &self.namespace); - let configmap_name = format!("bundle-{}", self.id); - let _ = configmaps - .delete(&configmap_name, &DeleteParams::default()) - .await; - - // Delete service if it exists - if let Some(endpoint) = self.service_endpoint.lock().await.as_ref() { - let services: Api = Api::namespaced(self.client.clone(), &self.namespace); - // Extract service name from hostname - let service_name = endpoint.host.split('.').next().unwrap_or("unknown"); - let _ = services - .delete(service_name, &DeleteParams::default()) - .await; - } - - Ok(()) - } -} diff --git a/crates/tower-runtime/src/backends/mod.rs b/crates/tower-runtime/src/backends/mod.rs deleted file mode 100644 index 7630f4cf..00000000 --- a/crates/tower-runtime/src/backends/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Concrete implementations of ExecutionBackend for different compute substrates - -pub mod subprocess; - -pub mod k8s; diff --git a/crates/tower-runtime/src/lib.rs b/crates/tower-runtime/src/lib.rs index 587c2ac6..74091edc 100644 --- a/crates/tower-runtime/src/lib.rs +++ b/crates/tower-runtime/src/lib.rs @@ -6,10 +6,10 @@ use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use tower_package::Package; -pub mod backends; pub mod errors; pub mod execution; pub mod local; +pub mod subprocess; use errors::Error; diff --git a/crates/tower-runtime/src/backends/subprocess.rs b/crates/tower-runtime/src/subprocess.rs similarity index 100% rename from crates/tower-runtime/src/backends/subprocess.rs rename to crates/tower-runtime/src/subprocess.rs From b0efacba0b5aa8e5ba8dd16792bcb8eb378413f4 Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Thu, 15 Jan 2026 12:13:32 +0100 Subject: [PATCH 20/21] revert session.json --- .../test-home/.config/tower/session.json | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/integration/test-home/.config/tower/session.json b/tests/integration/test-home/.config/tower/session.json index 469218e4..f2590a76 100644 --- a/tests/integration/test-home/.config/tower/session.json +++ b/tests/integration/test-home/.config/tower/session.json @@ -1 +1,20 @@ -{"tower_url":"http://127.0.0.1:8000/v1","user":{"email":"test@example.com","first_name":"Test","last_name":"User","created_at":"2023-01-01T00:00:00Z"},"token":{"jwt":"mock_jwt_token"},"active_team":{"name":"default","token":{"jwt":"mock_jwt_token"},"team_type":"user"},"teams":[{"name":"default","token":{"jwt":"mock_jwt_token"},"team_type":"user"}]} \ No newline at end of file +{ + "tower_url": "http://127.0.0.1:8000", + "user": { + "email": "test@example.com", + "first_name": "Test", + "last_name": "User", + "created_at": "2023-01-01T00:00:00Z" + }, + "token": { + "jwt": "mock_jwt_token" + }, + "active_team": { + "name": "test-team", + "token": { + "jwt": "mock_jwt_token" + }, + "team_type": "personal" + }, + "teams": [] +} \ No newline at end of file From 44e495643c6b1f985df31ca0bb21521828a56818 Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Thu, 15 Jan 2026 12:25:22 +0100 Subject: [PATCH 21/21] minro --- crates/tower-cmd/src/run.rs | 121 ++++++++++++++++++++---------------- 1 file changed, 66 insertions(+), 55 deletions(-) diff --git a/crates/tower-cmd/src/run.rs b/crates/tower-cmd/src/run.rs index f423616b..cea55397 100644 --- a/crates/tower-cmd/src/run.rs +++ b/crates/tower-cmd/src/run.rs @@ -10,12 +10,19 @@ use tower_telemetry::{debug, Context}; use crate::{api, output, util::dates}; use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; use tokio::sync::{ mpsc::Receiver as MpscReceiver, oneshot::{self, Receiver as OneshotReceiver}, Mutex, }; use tokio::time::{sleep, timeout, Duration}; +use tower_runtime::execution::ExecutionHandle; +use tower_runtime::execution::{ + CacheBackend, CacheConfig, CacheIsolation, ExecutionBackend, ExecutionSpec, PackageRef, + ResourceLimits, RuntimeConfig as ExecRuntimeConfig, +}; +use tower_runtime::subprocess::SubprocessBackend; pub fn run_cmd() -> Command { Command::new("run") @@ -147,7 +154,7 @@ where env_vars.insert("TOWER_URL".to_string(), config.tower_url.to_string()); // There should always be a session, if there isn't one then I'm not sure how we got here? - let session = config.session.ok_or(Error::NoSession)?; + let session = config.session.as_ref().ok_or(Error::NoSession)?; env_vars.insert("TOWER_JWT".to_string(), session.token.jwt.to_string()); @@ -163,23 +170,10 @@ where // Build the package let mut package = build_package(&towerfile).await?; - // Unpack the package package.unpack().await?; - output::success(&format!("Launching app `{}`", towerfile.app.name)); - - // Create backend and launch app using SubprocessBackend - use tower_runtime::execution::{ - CacheBackend, CacheConfig, CacheIsolation, ExecutionBackend, ExecutionSpec, PackageRef, - ResourceLimits, RuntimeConfig as ExecRuntimeConfig, - }; - use tower_runtime::subprocess::SubprocessBackend; - let backend = SubprocessBackend::new(config.cache_dir.clone()); - - // Build ExecutionSpec for SubprocessBackend - use std::time::{SystemTime, UNIX_EPOCH}; let run_id = format!( "cli-run-{}", SystemTime::now() @@ -187,7 +181,64 @@ where .unwrap() .as_nanos() ); + let handle = backend + .create(build_cli_execution_spec( + config, + env, + params, + secrets, + env_vars, + &mut package, + run_id, + )) + .await?; + let receiver = handle.logs().await?; + let output_task = tokio::spawn(output_handler(receiver)); + + // Monitor app status concurrently + let handle = Arc::new(Mutex::new(handle)); + let status_task = tokio::spawn(monitor_cli_status(Arc::clone(&handle))); + + // Wait for app to complete or SIGTERM + let status_result = tokio::select! { + status = status_task => { + debug!("Status task completed, result: {:?}", status); + status.unwrap() + }, + _ = tokio::signal::ctrl_c(), if !output::get_output_mode().is_mcp() => { + output::write("\nReceived Ctrl+C, stopping local run...\n"); + handle.lock().await.terminate().await.ok(); + return Ok(output_task.await.unwrap()); + } + }; + let final_result = output_task.await.unwrap(); + + // And if we crashed, err out + match status_result { + Status::Exited => output::success("Your local run exited cleanly."), + Status::Crashed { code } => { + output::error(&format!("Your local run crashed with exit code: {}", code)); + return Err(Error::AppCrashed); + } + _ => { + debug!("Unexpected status after monitoring: {:?}", status_result); + output::error("An unexpected error occurred while monitoring your local run status!"); + return Err(Error::AppCrashed); + } + } + + Ok(final_result) +} +fn build_cli_execution_spec( + config: Config, + env: &str, + params: HashMap, + secrets: HashMap, + env_vars: HashMap, + package: &mut Package, + run_id: String, +) -> ExecutionSpec { let spec = ExecutionSpec { id: run_id, package: PackageRef::Local { @@ -227,47 +278,7 @@ where networking: None, telemetry_ctx: Context::new(), }; - - let handle = backend.create(spec).await?; - - // Get log receiver from handle - use tower_runtime::execution::ExecutionHandle as _; - let receiver = handle.logs().await?; - let output_task = tokio::spawn(output_handler(receiver)); - - // Monitor app status concurrently - let handle = Arc::new(Mutex::new(handle)); - let status_task = tokio::spawn(monitor_cli_status(Arc::clone(&handle))); - - // Wait for app to complete or SIGTERM - let status_result = tokio::select! { - status = status_task => { - debug!("Status task completed, result: {:?}", status); - status.unwrap() - }, - _ = tokio::signal::ctrl_c(), if !output::get_output_mode().is_mcp() => { - output::write("\nReceived Ctrl+C, stopping local run...\n"); - handle.lock().await.terminate().await.ok(); - return Ok(output_task.await.unwrap()); - } - }; - let final_result = output_task.await.unwrap(); - - // And if we crashed, err out - match status_result { - Status::Exited => output::success("Your local run exited cleanly."), - Status::Crashed { code } => { - output::error(&format!("Your local run crashed with exit code: {}", code)); - return Err(Error::AppCrashed); - } - _ => { - debug!("Unexpected status after monitoring: {:?}", status_result); - output::error("An unexpected error occurred while monitoring your local run status!"); - return Err(Error::AppCrashed); - } - } - - Ok(final_result) + spec } /// do_run_local is the entrypoint for running an app locally. It will load the Towerfile, build