mirror of
https://github.com/moghtech/komodo.git
synced 2026-10-31 05:05:57 -05:00
Compare commits
57
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c76b8487f | ||
|
|
2b32d9042a | ||
|
|
dc48f1f2ca | ||
|
|
8e7b7bdcf1 | ||
|
|
f11d64f72e | ||
|
|
2ffae85180 | ||
|
|
bd79d0f1e0 | ||
|
|
e890b1f675 | ||
|
|
3b7de25c30 | ||
|
|
793bb99f31 | ||
|
|
d465c9f273 | ||
|
|
ce641a8974 | ||
|
|
1b89ceb122 | ||
|
|
2dbc011d26 | ||
|
|
246da88ae1 | ||
|
|
a8c16f64b1 | ||
|
|
a5b711a348 | ||
|
|
9666e9ad83 | ||
|
|
7479640c73 | ||
|
|
4823825035 | ||
|
|
23897a7acf | ||
|
|
20d5588b5c | ||
|
|
f7e15ccde5 | ||
|
|
cf7623b1fc | ||
|
|
d3c464c05d | ||
|
|
5c9d416aa4 | ||
|
|
aabcd88312 | ||
|
|
9d2624c6bc | ||
|
|
ee11fb0b6c | ||
|
|
45adfbddd0 | ||
|
|
d26d035dc6 | ||
|
|
e673ba0adf | ||
|
|
f876facfa7 | ||
|
|
3a47d57478 | ||
|
|
a707028277 | ||
|
|
0c6276c677 | ||
|
|
fc9c6706f1 | ||
|
|
7674269ce9 | ||
|
|
3b511c5adc | ||
|
|
87221a10e9 | ||
|
|
450cb6a148 | ||
|
|
f252cefb21 | ||
|
|
7855e9d688 | ||
|
|
feb263c15f | ||
|
|
4f8d1c22cc | ||
|
|
60bd47834e | ||
|
|
4d632a6b61 | ||
|
|
381dd76723 | ||
|
|
077e28a5fe | ||
|
|
6b02aaed7d | ||
|
|
e466944c05 | ||
|
|
8ff94b7465 | ||
|
|
b17df5ed7b | ||
|
|
207dc30206 | ||
|
|
c3eb386bdb | ||
|
|
4279e46892 | ||
|
|
8d3d2fee12 |
Generated
+229
-125
@@ -2,15 +2,6 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
@@ -287,9 +278,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-ec2"
|
||||
version = "1.173.0"
|
||||
version = "1.175.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8daa0257ec1eb83217a286d91f836e7a1f82e885a6c890a8c9ff28a7f8dc71bf"
|
||||
checksum = "c1515b09ab70c0991c0b002b0e9114a3ee73b7c5d3d65dfdb047278b208f21b0"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -449,7 +440,7 @@ dependencies = [
|
||||
"hyper-util",
|
||||
"pin-project-lite",
|
||||
"rustls 0.21.12",
|
||||
"rustls 0.23.32",
|
||||
"rustls 0.23.33",
|
||||
"rustls-native-certs 0.8.1",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
@@ -680,7 +671,7 @@ dependencies = [
|
||||
"hyper 1.7.0",
|
||||
"hyper-util",
|
||||
"pin-project-lite",
|
||||
"rustls 0.23.32",
|
||||
"rustls 0.23.33",
|
||||
"rustls-pemfile 2.2.0",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
@@ -688,21 +679,6 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base16ct"
|
||||
version = "0.2.0"
|
||||
@@ -885,7 +861,7 @@ dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"getrandom 0.3.3",
|
||||
"hex",
|
||||
"indexmap 2.11.4",
|
||||
"indexmap 2.12.0",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"rand 0.9.2",
|
||||
@@ -926,7 +902,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cache"
|
||||
version = "2.0.0-dev-59"
|
||||
version = "2.0.0-dev-72"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"tokio",
|
||||
@@ -1118,7 +1094,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "command"
|
||||
version = "2.0.0-dev-59"
|
||||
version = "2.0.0-dev-72"
|
||||
dependencies = [
|
||||
"komodo_client",
|
||||
"run_command",
|
||||
@@ -1144,10 +1120,10 @@ checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb"
|
||||
|
||||
[[package]]
|
||||
name = "config"
|
||||
version = "2.0.0-dev-59"
|
||||
version = "2.0.0-dev-72"
|
||||
dependencies = [
|
||||
"colored",
|
||||
"indexmap 2.11.4",
|
||||
"indexmap 2.12.0",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1189,6 +1165,15 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
@@ -1233,6 +1218,12 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "critical-section"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||
|
||||
[[package]]
|
||||
name = "croner"
|
||||
version = "3.0.0"
|
||||
@@ -1244,6 +1235,24 @@ dependencies = [
|
||||
"strum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
@@ -1258,9 +1267,13 @@ checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"crossterm_winapi",
|
||||
"derive_more 2.0.1",
|
||||
"document-features",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"rustix",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
@@ -1429,7 +1442,7 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
||||
|
||||
[[package]]
|
||||
name = "database"
|
||||
version = "2.0.0-dev-59"
|
||||
version = "2.0.0-dev-72"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compression",
|
||||
@@ -1556,13 +1569,34 @@ version = "0.99.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"convert_case 0.4.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
|
||||
dependencies = [
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
|
||||
dependencies = [
|
||||
"convert_case 0.7.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_variants"
|
||||
version = "1.0.0"
|
||||
@@ -1707,7 +1741,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "encoding"
|
||||
version = "2.0.0-dev-59"
|
||||
version = "2.0.0-dev-72"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -1749,7 +1783,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "environment"
|
||||
version = "2.0.0-dev-59"
|
||||
version = "2.0.0-dev-72"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"formatting",
|
||||
@@ -1759,7 +1793,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "environment_file"
|
||||
version = "2.0.0-dev-59"
|
||||
version = "2.0.0-dev-72"
|
||||
dependencies = [
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
@@ -1855,7 +1889,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "formatting"
|
||||
version = "2.0.0-dev-59"
|
||||
version = "2.0.0-dev-72"
|
||||
dependencies = [
|
||||
"serror",
|
||||
]
|
||||
@@ -2019,15 +2053,9 @@ dependencies = [
|
||||
"polyval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.32.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
|
||||
|
||||
[[package]]
|
||||
name = "git"
|
||||
version = "2.0.0-dev-59"
|
||||
version = "2.0.0-dev-72"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cache",
|
||||
@@ -2067,7 +2095,7 @@ dependencies = [
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http 0.2.12",
|
||||
"indexmap 2.11.4",
|
||||
"indexmap 2.12.0",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -2086,7 +2114,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http 1.3.1",
|
||||
"indexmap 2.11.4",
|
||||
"indexmap 2.12.0",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -2180,6 +2208,31 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hickory-proto"
|
||||
version = "0.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"cfg-if",
|
||||
"data-encoding",
|
||||
"enum-as-inner",
|
||||
"futures-channel",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"idna",
|
||||
"ipnet",
|
||||
"once_cell",
|
||||
"rand 0.9.2",
|
||||
"ring",
|
||||
"thiserror 2.0.17",
|
||||
"tinyvec",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hickory-resolver"
|
||||
version = "0.24.4"
|
||||
@@ -2188,7 +2241,7 @@ checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"hickory-proto",
|
||||
"hickory-proto 0.24.4",
|
||||
"ipconfig",
|
||||
"lru-cache",
|
||||
"once_cell",
|
||||
@@ -2201,6 +2254,27 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hickory-resolver"
|
||||
version = "0.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"hickory-proto 0.25.2",
|
||||
"ipconfig",
|
||||
"moka",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"rand 0.9.2",
|
||||
"resolv-conf",
|
||||
"smallvec",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.12.4"
|
||||
@@ -2380,7 +2454,7 @@ dependencies = [
|
||||
"http 1.3.1",
|
||||
"hyper 1.7.0",
|
||||
"hyper-util",
|
||||
"rustls 0.23.32",
|
||||
"rustls 0.23.33",
|
||||
"rustls-native-certs 0.8.1",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
@@ -2593,9 +2667,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.11.4"
|
||||
version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
||||
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.0",
|
||||
@@ -2614,24 +2688,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "interpolate"
|
||||
version = "2.0.0-dev-59"
|
||||
version = "2.0.0-dev-72"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
"svi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-uring"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipconfig"
|
||||
version = "0.3.2"
|
||||
@@ -2747,14 +2810,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_cli"
|
||||
version = "2.0.0-dev-59"
|
||||
version = "2.0.0-dev-72"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"clap",
|
||||
"colored",
|
||||
"comfy-table",
|
||||
"config",
|
||||
"crossterm",
|
||||
"database",
|
||||
"dotenvy",
|
||||
"environment_file",
|
||||
@@ -2767,13 +2832,14 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_qs",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tracing",
|
||||
"wildcard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "komodo_client"
|
||||
version = "2.0.0-dev-59"
|
||||
version = "2.0.0-dev-72"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2784,8 +2850,8 @@ dependencies = [
|
||||
"derive_empty_traits",
|
||||
"derive_variants",
|
||||
"envy",
|
||||
"futures",
|
||||
"indexmap 2.11.4",
|
||||
"futures-util",
|
||||
"indexmap 2.12.0",
|
||||
"ipnetwork",
|
||||
"mongo_indexed",
|
||||
"partial_derive2",
|
||||
@@ -2804,12 +2870,11 @@ dependencies = [
|
||||
"tracing",
|
||||
"typeshare",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "komodo_core"
|
||||
version = "2.0.0-dev-59"
|
||||
version = "2.0.0-dev-72"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -2839,11 +2904,11 @@ dependencies = [
|
||||
"environment_file",
|
||||
"envy",
|
||||
"formatting",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"git",
|
||||
"hex",
|
||||
"hmac",
|
||||
"indexmap 2.11.4",
|
||||
"indexmap 2.12.0",
|
||||
"interpolate",
|
||||
"jsonwebtoken",
|
||||
"komodo_client",
|
||||
@@ -2856,7 +2921,7 @@ dependencies = [
|
||||
"reqwest",
|
||||
"resolver_api",
|
||||
"response",
|
||||
"rustls 0.23.32",
|
||||
"rustls 0.23.33",
|
||||
"secret_file",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2882,7 +2947,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_periphery"
|
||||
version = "2.0.0-dev-59"
|
||||
version = "2.0.0-dev-72"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -2903,8 +2968,9 @@ dependencies = [
|
||||
"environment_file",
|
||||
"envy",
|
||||
"formatting",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"git",
|
||||
"hickory-resolver 0.25.2",
|
||||
"interpolate",
|
||||
"komodo_client",
|
||||
"logger",
|
||||
@@ -2913,7 +2979,7 @@ dependencies = [
|
||||
"portable-pty",
|
||||
"resolver_api",
|
||||
"run_command",
|
||||
"rustls 0.23.32",
|
||||
"rustls 0.23.33",
|
||||
"secret_file",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3002,7 +3068,7 @@ checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
|
||||
[[package]]
|
||||
name = "logger"
|
||||
version = "2.0.0-dev-59"
|
||||
version = "2.0.0-dev-72"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
@@ -3139,10 +3205,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moka"
|
||||
version = "0.12.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
"equivalent",
|
||||
"parking_lot",
|
||||
"portable-atomic",
|
||||
"rustc_version",
|
||||
"smallvec",
|
||||
"tagptr",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mongo_indexed"
|
||||
version = "2.0.2"
|
||||
@@ -3196,14 +3281,14 @@ dependencies = [
|
||||
"bson",
|
||||
"chrono",
|
||||
"derive-where",
|
||||
"derive_more",
|
||||
"derive_more 0.99.20",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"hickory-proto",
|
||||
"hickory-resolver",
|
||||
"hickory-proto 0.24.4",
|
||||
"hickory-resolver 0.24.4",
|
||||
"hmac",
|
||||
"macro_magic",
|
||||
"md-5",
|
||||
@@ -3214,7 +3299,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"rand 0.8.5",
|
||||
"rustc_version_runtime",
|
||||
"rustls 0.23.32",
|
||||
"rustls 0.23.33",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
@@ -3275,7 +3360,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "noise"
|
||||
version = "2.0.0-dev-59"
|
||||
version = "2.0.0-dev-72"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -3422,20 +3507,15 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.37.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
dependencies = [
|
||||
"critical-section",
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
@@ -3695,7 +3775,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "periphery_client"
|
||||
version = "2.0.0-dev-59"
|
||||
version = "2.0.0-dev-72"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"derive_variants",
|
||||
@@ -3801,6 +3881,12 @@ dependencies = [
|
||||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
|
||||
|
||||
[[package]]
|
||||
name = "portable-pty"
|
||||
version = "0.9.0"
|
||||
@@ -3909,7 +3995,7 @@ dependencies = [
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls 0.23.32",
|
||||
"rustls 0.23.33",
|
||||
"socket2 0.6.1",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
@@ -3929,7 +4015,7 @@ dependencies = [
|
||||
"rand 0.9.2",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls 0.23.32",
|
||||
"rustls 0.23.33",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.17",
|
||||
@@ -4121,7 +4207,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls 0.23.32",
|
||||
"rustls 0.23.33",
|
||||
"rustls-native-certs 0.8.1",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
@@ -4170,7 +4256,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "response"
|
||||
version = "2.0.0-dev-59"
|
||||
version = "2.0.0-dev-72"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -4232,12 +4318,6 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
@@ -4290,9 +4370,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.32"
|
||||
version = "0.23.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40"
|
||||
checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"log",
|
||||
@@ -4455,7 +4535,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "secret_file"
|
||||
version = "2.0.0-dev-59"
|
||||
version = "2.0.0-dev-72"
|
||||
dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
@@ -4558,7 +4638,7 @@ version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||
dependencies = [
|
||||
"indexmap 2.11.4",
|
||||
"indexmap 2.12.0",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
@@ -4639,7 +4719,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.11.4",
|
||||
"indexmap 2.12.0",
|
||||
"schemars 0.9.0",
|
||||
"schemars 1.0.4",
|
||||
"serde_core",
|
||||
@@ -4666,7 +4746,7 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f"
|
||||
dependencies = [
|
||||
"indexmap 2.11.4",
|
||||
"indexmap 2.12.0",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
@@ -4755,6 +4835,27 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.6"
|
||||
@@ -4806,9 +4907,9 @@ checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
|
||||
|
||||
[[package]]
|
||||
name = "slack_client_rs"
|
||||
version = "1.2.1"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "295af69963fd1298940a45be3cefa447285f7cd71970f4a47e54be26771a7be6"
|
||||
checksum = "5e52d695d2a62bb62481567a1fafc9d83af9f2c469fa37872136e0171e6b3936"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"reqwest",
|
||||
@@ -5004,6 +5105,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tagptr"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
|
||||
|
||||
[[package]]
|
||||
name = "take_mut"
|
||||
version = "0.2.2"
|
||||
@@ -5132,29 +5239,26 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.47.1"
|
||||
version = "1.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
|
||||
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"io-uring",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"socket2 0.6.1",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.5.0"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -5177,7 +5281,7 @@ version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||
dependencies = [
|
||||
"rustls 0.23.32",
|
||||
"rustls 0.23.33",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -5201,7 +5305,7 @@ checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"rustls 0.23.32",
|
||||
"rustls 0.23.33",
|
||||
"rustls-native-certs 0.8.1",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
@@ -5229,7 +5333,7 @@ version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
|
||||
dependencies = [
|
||||
"indexmap 2.11.4",
|
||||
"indexmap 2.12.0",
|
||||
"serde_core",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
@@ -5321,7 +5425,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"indexmap 2.11.4",
|
||||
"indexmap 2.12.0",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
"sync_wrapper",
|
||||
@@ -5464,7 +5568,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "transport"
|
||||
version = "2.0.0-dev-59"
|
||||
version = "2.0.0-dev-72"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -5476,7 +5580,7 @@ dependencies = [
|
||||
"periphery_client",
|
||||
"pin-project-lite",
|
||||
"rand 0.9.2",
|
||||
"rustls 0.23.32",
|
||||
"rustls 0.23.33",
|
||||
"serde",
|
||||
"serror",
|
||||
"sha1",
|
||||
@@ -5507,7 +5611,7 @@ dependencies = [
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.9.2",
|
||||
"rustls 0.23.32",
|
||||
"rustls 0.23.33",
|
||||
"rustls-pki-types",
|
||||
"sha1",
|
||||
"thiserror 2.0.17",
|
||||
|
||||
+9
-8
@@ -8,7 +8,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "2.0.0-dev-59"
|
||||
version = "2.0.0-dev-72"
|
||||
edition = "2024"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -41,7 +41,7 @@ git = { path = "lib/git" }
|
||||
# MOGH
|
||||
run_command = { version = "0.0.6", features = ["async_tokio"] }
|
||||
serror = { version = "0.5.3", default-features = false }
|
||||
slack = { version = "1.2.1", package = "slack_client_rs", default-features = false, features = ["rustls"] }
|
||||
slack = { version = "2.0.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
|
||||
derive_default_builder = "0.1.8"
|
||||
derive_empty_traits = "0.1.0"
|
||||
async_timing_util = "1.1.0"
|
||||
@@ -55,11 +55,10 @@ svi = "1.2.0"
|
||||
|
||||
# ASYNC
|
||||
reqwest = { version = "0.12.24", default-features = false, features = ["json", "stream", "rustls-tls-native-roots"] }
|
||||
tokio = { version = "1.47.1", features = ["full"] }
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
tokio-util = { version = "0.7.16", features = ["io", "codec"] }
|
||||
tokio-stream = { version = "0.1.17", features = ["sync"] }
|
||||
pin-project-lite = "0.2.16"
|
||||
futures = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
arc-swap = "1.7.1"
|
||||
|
||||
@@ -72,7 +71,7 @@ axum = { version = "0.8.6", features = ["ws", "json", "macros"] }
|
||||
|
||||
# SER/DE
|
||||
ipnetwork = { version = "0.21.1", features = ["serde"] }
|
||||
indexmap = { version = "2.11.4", features = ["serde"] }
|
||||
indexmap = { version = "2.12.0", features = ["serde"] }
|
||||
serde = { version = "1.0.227", features = ["derive"] }
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
bson = { version = "2.15.0" } # must keep in sync with mongodb version
|
||||
@@ -103,7 +102,7 @@ envy = "0.4.2"
|
||||
# CRYPTO / AUTH
|
||||
uuid = { version = "1.18.1", features = ["v4", "fast-rng", "serde"] }
|
||||
jsonwebtoken = { version = "10.0.0", features = ["aws_lc_rs"] } # locked back with octorust
|
||||
rustls = { version = "0.23.32", features = ["aws-lc-rs"] }
|
||||
rustls = { version = "0.23.33", features = ["aws-lc-rs"] }
|
||||
pem-rfc7468 = { version = "0.7.0", features = ["alloc"] }
|
||||
openidconnect = "4.0.1"
|
||||
urlencoding = "2.1.3"
|
||||
@@ -120,13 +119,16 @@ spki = "0.7.3"
|
||||
der = "0.7.10"
|
||||
|
||||
# SYSTEM
|
||||
hickory-resolver = "0.25.2"
|
||||
portable-pty = "0.9.0"
|
||||
shell-escape = "0.1.5"
|
||||
crossterm = "0.29.0"
|
||||
bollard = "0.19.3"
|
||||
sysinfo = "0.37.1"
|
||||
|
||||
# CLOUD
|
||||
aws-config = "1.8.8"
|
||||
aws-sdk-ec2 = "1.173.0"
|
||||
aws-sdk-ec2 = "1.175.1"
|
||||
aws-credential-types = "1.2.8"
|
||||
|
||||
## CRON
|
||||
@@ -138,7 +140,6 @@ croner = "3.0.0"
|
||||
# MISC
|
||||
async-compression = { version = "0.4.32", features = ["tokio", "gzip"] }
|
||||
derive_builder = "0.20.2"
|
||||
shell-escape = "0.1.5"
|
||||
comfy-table = "7.2.1"
|
||||
typeshare = "1.0.4"
|
||||
dashmap = "6.1.0"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
const cmd = "km run -y action deploy-komodo-fe-change";
|
||||
new Deno.Command("bash", {
|
||||
args: ["-c", cmd],
|
||||
}).spawn();
|
||||
@@ -21,9 +21,11 @@ config.workspace = true
|
||||
logger.workspace = true
|
||||
noise.workspace = true
|
||||
# external
|
||||
tokio-tungstenite.workspace = true
|
||||
futures-util.workspace = true
|
||||
comfy-table.workspace = true
|
||||
serde_json.workspace = true
|
||||
crossterm.workspace = true
|
||||
serde_qs.workspace = true
|
||||
wildcard.workspace = true
|
||||
tracing.workspace = true
|
||||
@@ -31,6 +33,7 @@ colored.workspace = true
|
||||
dotenvy.workspace = true
|
||||
anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
bytes.workspace = true
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
clap.workspace = true
|
||||
|
||||
@@ -5,9 +5,10 @@ use futures_util::{FutureExt, try_join};
|
||||
use komodo_client::{
|
||||
KomodoClient,
|
||||
api::read::{
|
||||
ListActions, ListAlerters, ListBuilders, ListBuilds,
|
||||
ListDeployments, ListProcedures, ListRepos, ListResourceSyncs,
|
||||
ListSchedules, ListServers, ListStacks, ListTags,
|
||||
ListActions, ListAlerters, ListAllTerminals, ListBuilders,
|
||||
ListBuilds, ListDeployments, ListProcedures, ListRepos,
|
||||
ListResourceSyncs, ListSchedules, ListServers, ListStacks,
|
||||
ListTags,
|
||||
},
|
||||
entities::{
|
||||
ResourceTargetVariant,
|
||||
@@ -26,10 +27,15 @@ use komodo_client::{
|
||||
ProcedureListItem, ProcedureListItemInfo, ProcedureState,
|
||||
},
|
||||
repo::{RepoListItem, RepoListItemInfo, RepoState},
|
||||
resource::{ResourceListItem, ResourceQuery},
|
||||
resource::{
|
||||
ResourceListItem, ResourceQuery, TemplatesQueryBehavior,
|
||||
},
|
||||
resource_link,
|
||||
schedule::Schedule,
|
||||
server::{ServerListItem, ServerListItemInfo, ServerState},
|
||||
server::{
|
||||
ServerListItem, ServerListItemInfo, ServerState,
|
||||
TerminalInfoWithServer,
|
||||
},
|
||||
stack::{StackListItem, StackListItemInfo, StackState},
|
||||
sync::{
|
||||
ResourceSyncListItem, ResourceSyncListItemInfo,
|
||||
@@ -74,15 +80,18 @@ pub async fn handle(list: &args::list::List) -> anyhow::Result<()> {
|
||||
Some(ListCommand::Syncs(filters)) => {
|
||||
list_resources::<ResourceSyncListItem>(filters, false).await
|
||||
}
|
||||
Some(ListCommand::Terminals(filters)) => {
|
||||
list_terminals(filters).await
|
||||
}
|
||||
Some(ListCommand::Schedules(filters)) => {
|
||||
list_schedules(filters).await
|
||||
}
|
||||
Some(ListCommand::Builders(filters)) => {
|
||||
list_resources::<BuilderListItem>(filters, false).await
|
||||
}
|
||||
Some(ListCommand::Alerters(filters)) => {
|
||||
list_resources::<AlerterListItem>(filters, false).await
|
||||
}
|
||||
Some(ListCommand::Schedules(filters)) => {
|
||||
list_schedules(filters).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +198,40 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_terminals(
|
||||
filters: &ResourceFilters,
|
||||
) -> anyhow::Result<()> {
|
||||
let client = crate::command::komodo_client().await?;
|
||||
let query = ResourceQuery::builder()
|
||||
.tags(filters.tags.clone())
|
||||
.templates(TemplatesQueryBehavior::Exclude)
|
||||
.build();
|
||||
let (mut terminals, servers) = tokio::try_join!(
|
||||
client.read(ListAllTerminals {
|
||||
query: query.clone(),
|
||||
fresh: true,
|
||||
}),
|
||||
client
|
||||
.read(ListServers { query })
|
||||
.map(|res| res.map(|res| res
|
||||
.into_iter()
|
||||
.map(|t| (t.id, t.name))
|
||||
.collect::<HashMap<_, _>>()))
|
||||
)?;
|
||||
// Fix server ids -> names
|
||||
terminals.iter_mut().for_each(|terminal| {
|
||||
let Some(name) = servers.get(&terminal.server_id) else {
|
||||
terminal.server_id = String::new();
|
||||
return;
|
||||
};
|
||||
terminal.server_id.clone_from(name);
|
||||
});
|
||||
if !terminals.is_empty() {
|
||||
print_items(terminals, filters.format, filters.links)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_schedules(
|
||||
filters: &ResourceFilters,
|
||||
) -> anyhow::Result<()> {
|
||||
@@ -1134,6 +1177,28 @@ impl PrintTable for ResourceListItem<AlerterListItemInfo> {
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for TerminalInfoWithServer {
|
||||
fn header(_links: bool) -> &'static [&'static str] {
|
||||
&["Terminal", "Server", "Command", "Size", "Created"]
|
||||
}
|
||||
fn row(self, _links: bool) -> Vec<comfy_table::Cell> {
|
||||
vec![
|
||||
Cell::new(self.name).add_attribute(Attribute::Bold),
|
||||
Cell::new(self.server_id),
|
||||
Cell::new(self.command),
|
||||
Cell::new(if self.stored_size_kb < 1.0 {
|
||||
format!("{:.1} KiB", self.stored_size_kb)
|
||||
} else {
|
||||
format!("{:.} KiB", self.stored_size_kb)
|
||||
}),
|
||||
Cell::new(
|
||||
format_timetamp(self.created_at)
|
||||
.unwrap_or_else(|_| String::from("Invalid created at")),
|
||||
),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for Schedule {
|
||||
fn header(links: bool) -> &'static [&'static str] {
|
||||
if links {
|
||||
@@ -1146,7 +1211,7 @@ impl PrintTable for Schedule {
|
||||
let next_run = if let Some(ts) = self.next_scheduled_run {
|
||||
Cell::new(
|
||||
format_timetamp(ts)
|
||||
.unwrap_or(String::from("Invalid next ts")),
|
||||
.unwrap_or_else(|_| String::from("Invalid next ts")),
|
||||
)
|
||||
.add_attribute(Attribute::Bold)
|
||||
} else {
|
||||
|
||||
@@ -18,6 +18,7 @@ pub mod container;
|
||||
pub mod database;
|
||||
pub mod execute;
|
||||
pub mod list;
|
||||
pub mod ssh;
|
||||
pub mod update;
|
||||
|
||||
async fn komodo_client() -> anyhow::Result<&'static KomodoClient> {
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
use anyhow::Context;
|
||||
use bytes::Bytes;
|
||||
use colored::Colorize;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use komodo_client::{
|
||||
api::write::{CreateTerminal, TerminalRecreateMode},
|
||||
entities::config::cli::args::ssh::Ssh,
|
||||
};
|
||||
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
|
||||
use tokio_tungstenite::tungstenite;
|
||||
|
||||
pub async fn handle(
|
||||
Ssh {
|
||||
server,
|
||||
name,
|
||||
command,
|
||||
recreate,
|
||||
}: &Ssh,
|
||||
) -> anyhow::Result<()> {
|
||||
// Need to forward multiple sources into ws write
|
||||
let (write_tx, mut write_rx) =
|
||||
tokio::sync::mpsc::channel::<Bytes>(1024);
|
||||
|
||||
// ================
|
||||
// SETUP RESIZING
|
||||
// ================
|
||||
|
||||
// Subscribe to SIGWINCH for resize messages
|
||||
let mut sigwinch = tokio::signal::unix::signal(
|
||||
tokio::signal::unix::SignalKind::window_change(),
|
||||
)
|
||||
.context("failed to register SIGWINCH handler")?;
|
||||
|
||||
// Send first resize messsage, bailing if it fails to get the size.
|
||||
write_tx.send(resize_message()?).await?;
|
||||
|
||||
let forward_resize = async {
|
||||
while sigwinch.recv().await.is_some() {
|
||||
if let Ok(resize_message) = resize_message()
|
||||
&& write_tx.send(resize_message).await.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let forward_stdin = async {
|
||||
let mut stdin = tokio::io::stdin();
|
||||
let mut buf = [0u8; 8192];
|
||||
loop {
|
||||
// Read into buffer starting from index 1,
|
||||
// leaving first byte to represent 'data' message.
|
||||
let n = match stdin.read(&mut buf[1..]).await {
|
||||
Ok(0) => break, // EOF
|
||||
Ok(n) => n,
|
||||
Err(_) => break,
|
||||
};
|
||||
// Check for disconnect sequence (alt + q)
|
||||
if buf[1..(n + 1)] == [197, 147] {
|
||||
break;
|
||||
}
|
||||
let bytes = Bytes::copy_from_slice(&buf[..(n + 1)]);
|
||||
if write_tx.send(bytes).await.is_err() {
|
||||
break;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// =====================
|
||||
// CONNECT AND FORWARD
|
||||
// =====================
|
||||
|
||||
let client = super::komodo_client().await?;
|
||||
|
||||
// Init the terminal if it doesn't exist already.
|
||||
client
|
||||
.write(CreateTerminal {
|
||||
server: server.to_string(),
|
||||
name: name.to_string(),
|
||||
command: command.clone(),
|
||||
recreate: if *recreate {
|
||||
TerminalRecreateMode::Always
|
||||
} else {
|
||||
TerminalRecreateMode::DifferentCommand
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
let (mut ws_write, mut ws_read) = client
|
||||
.connect_terminal_websocket(server, name)
|
||||
.await?
|
||||
.split();
|
||||
|
||||
let forward_write = async {
|
||||
while let Some(bytes) = write_rx.recv().await {
|
||||
if let Err(e) =
|
||||
ws_write.send(tungstenite::Message::Binary(bytes)).await
|
||||
{
|
||||
return Some(e);
|
||||
};
|
||||
}
|
||||
None
|
||||
};
|
||||
|
||||
let forward_read = async {
|
||||
let mut stdout = tokio::io::stdout();
|
||||
loop {
|
||||
match ws_read.next().await {
|
||||
Some(Ok(tungstenite::Message::Binary(bytes))) => {
|
||||
if let Err(e) =
|
||||
tokio::io::copy(&mut bytes.as_ref(), &mut stdout)
|
||||
.await
|
||||
.context("Failed to copy bytes to stdout")
|
||||
{
|
||||
return Some(e);
|
||||
}
|
||||
let _ = stdout.flush().await;
|
||||
}
|
||||
Some(Ok(tungstenite::Message::Text(text))) => {
|
||||
if let Err(e) =
|
||||
tokio::io::copy(&mut text.as_ref(), &mut stdout)
|
||||
.await
|
||||
.context("Failed to copy text to stdout")
|
||||
{
|
||||
return Some(e);
|
||||
}
|
||||
let _ = stdout.flush().await;
|
||||
}
|
||||
Some(Ok(tungstenite::Message::Close(_))) => break,
|
||||
Some(Err(e)) => {
|
||||
return Some(
|
||||
anyhow::Error::from(e).context("Websocket read error"),
|
||||
);
|
||||
}
|
||||
None => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
};
|
||||
|
||||
let guard = RawModeGuard::enable_raw_mode()?;
|
||||
|
||||
tokio::select! {
|
||||
_ = forward_resize => drop(guard),
|
||||
_ = forward_stdin => drop(guard),
|
||||
e = forward_write => {
|
||||
drop(guard);
|
||||
if let Some(e) = e {
|
||||
eprintln!("\nFailed to forward stdin | {e:#}");
|
||||
}
|
||||
},
|
||||
e = forward_read => {
|
||||
drop(guard);
|
||||
if let Some(e) = e {
|
||||
eprintln!("\nFailed to forward stdout | {e:#}");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
println!("\n\n{} {}", "connection".bold(), "closed".red().bold());
|
||||
|
||||
// It doesn't seem to exit by itself after the raw mode stuff.
|
||||
std::process::exit(0)
|
||||
}
|
||||
|
||||
fn resize_message() -> anyhow::Result<Bytes> {
|
||||
let (cols, rows) = crossterm::terminal::size()
|
||||
.context("Failed to get terminal size")?;
|
||||
let bytes: Vec<u8> =
|
||||
format!(r#"{{"rows":{rows},"cols":{cols}}}"#).into();
|
||||
let mut msg = Vec::with_capacity(bytes.len() + 1);
|
||||
msg.push(0xff); // resize prefix
|
||||
msg.extend(bytes);
|
||||
Ok(msg.into())
|
||||
}
|
||||
|
||||
struct RawModeGuard;
|
||||
|
||||
impl RawModeGuard {
|
||||
fn enable_raw_mode() -> anyhow::Result<Self> {
|
||||
crossterm::terminal::enable_raw_mode()
|
||||
.context("Failed to enable terminal raw mode")?;
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
impl Drop for RawModeGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Err(e) = crossterm::terminal::disable_raw_mode() {
|
||||
eprintln!("Failed to disable terminal raw mode | {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-6
@@ -41,12 +41,6 @@ async fn app() -> anyhow::Result<()> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
args::Command::Key { command } => {
|
||||
noise::key::command::handle(command).await
|
||||
}
|
||||
args::Command::Database { command } => {
|
||||
command::database::handle(command).await
|
||||
}
|
||||
args::Command::Container(container) => {
|
||||
command::container::handle(container).await
|
||||
}
|
||||
@@ -60,6 +54,13 @@ async fn app() -> anyhow::Result<()> {
|
||||
args::Command::Update { command } => {
|
||||
command::update::handle(command).await
|
||||
}
|
||||
args::Command::Ssh(ssh) => command::ssh::handle(ssh).await,
|
||||
args::Command::Key { command } => {
|
||||
noise::key::command::handle(command).await
|
||||
}
|
||||
args::Command::Database { command } => {
|
||||
command::database::handle(command).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -45,6 +45,7 @@ aws-credential-types.workspace = true
|
||||
english-to-cron.workspace = true
|
||||
openidconnect.workspace = true
|
||||
jsonwebtoken.workspace = true
|
||||
futures-util.workspace = true
|
||||
axum-server.workspace = true
|
||||
urlencoding.workspace = true
|
||||
aws-sdk-ec2.workspace = true
|
||||
@@ -63,7 +64,6 @@ colored.workspace = true
|
||||
dashmap.workspace = true
|
||||
tracing.workspace = true
|
||||
reqwest.workspace = true
|
||||
futures.workspace = true
|
||||
dotenvy.workspace = true
|
||||
anyhow.workspace = true
|
||||
croner.workspace = true
|
||||
|
||||
@@ -48,6 +48,9 @@ RUN mkdir /action-cache && \
|
||||
cd /action-cache && \
|
||||
deno install jsr:@std/yaml jsr:@std/toml
|
||||
|
||||
COPY ./bin/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
# Hint at the port
|
||||
EXPOSE 9120
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ COPY --from=x86_64 /core /app/core/linux/amd64
|
||||
COPY --from=aarch64 /core /app/core/linux/arm64
|
||||
RUN mv /app/core/${TARGETPLATFORM} /usr/local/bin/core && rm -r /app/core
|
||||
|
||||
# Same for util
|
||||
# Same for km
|
||||
COPY --from=x86_64 /km /app/km/linux/amd64
|
||||
COPY --from=aarch64 /km /app/km/linux/arm64
|
||||
RUN mv /app/km/${TARGETPLATFORM} /usr/local/bin/km && rm -r /app/km
|
||||
@@ -44,6 +44,9 @@ RUN mkdir /action-cache && \
|
||||
cd /action-cache && \
|
||||
deno install jsr:@std/yaml jsr:@std/toml
|
||||
|
||||
COPY ./bin/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
# Hint at the port
|
||||
EXPOSE 9120
|
||||
|
||||
@@ -51,7 +54,8 @@ ENV KOMODO_CLI_CONFIG_PATHS="/config"
|
||||
# This ensures any `komodo.cli.*` takes precedence over the Core `/config/*config.*`
|
||||
ENV KOMODO_CLI_CONFIG_KEYWORDS="*config.*,*komodo.cli*.*"
|
||||
|
||||
CMD [ "/bin/bash", "-c", "update-ca-certificates && core" ]
|
||||
ENTRYPOINT [ "entrypoint.sh" ]
|
||||
CMD [ "core" ]
|
||||
|
||||
# Label to prevent Komodo from stopping with StopAllContainers
|
||||
LABEL komodo.skip="true"
|
||||
|
||||
@@ -33,6 +33,9 @@ RUN mkdir /action-cache && \
|
||||
cd /action-cache && \
|
||||
deno install jsr:@std/yaml jsr:@std/toml
|
||||
|
||||
COPY ./bin/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
# Hint at the port
|
||||
EXPOSE 9120
|
||||
|
||||
@@ -40,7 +43,8 @@ ENV KOMODO_CLI_CONFIG_PATHS="/config"
|
||||
# This ensures any `komodo.cli.*` takes precedence over the Core `/config/*config.*`
|
||||
ENV KOMODO_CLI_CONFIG_KEYWORDS="*config.*,*komodo.cli*.*"
|
||||
|
||||
CMD [ "/bin/bash", "-c", "update-ca-certificates && core" ]
|
||||
ENTRYPOINT [ "entrypoint.sh" ]
|
||||
CMD [ "core" ]
|
||||
|
||||
# Label to prevent Komodo from stopping with StopAllContainers
|
||||
LABEL komodo.skip="true"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mungos::{find::find_collect, mongodb::bson::doc};
|
||||
use derive_variants::ExtractVariant;
|
||||
use futures::future::join_all;
|
||||
use futures_util::future::join_all;
|
||||
use interpolate::Interpolator;
|
||||
use komodo_client::entities::{
|
||||
ResourceTargetVariant,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use formatting::format_serror;
|
||||
use futures::{StreamExt, TryStreamExt, stream::FuturesUnordered};
|
||||
use futures_util::{
|
||||
StreamExt, TryStreamExt, stream::FuturesUnordered,
|
||||
};
|
||||
use komodo_client::{
|
||||
api::execute::{SendAlert, TestAlerter},
|
||||
entities::{
|
||||
|
||||
@@ -14,7 +14,7 @@ use database::mungos::{
|
||||
},
|
||||
};
|
||||
use formatting::format_serror;
|
||||
use futures::future::join_all;
|
||||
use futures_util::future::join_all;
|
||||
use interpolate::Interpolator;
|
||||
use komodo_client::{
|
||||
api::execute::{
|
||||
|
||||
@@ -7,7 +7,7 @@ use database::{
|
||||
mungos::find::find_collect,
|
||||
};
|
||||
use formatting::{bold, format_serror};
|
||||
use futures::{StreamExt, stream::FuturesOrdered};
|
||||
use futures_util::{StreamExt, stream::FuturesOrdered};
|
||||
use komodo_client::{
|
||||
api::execute::{
|
||||
BackupCoreDatabase, ClearRepoCache, GlobalAutoUpdate,
|
||||
|
||||
@@ -8,7 +8,7 @@ use axum_extra::{TypedHeader, headers::ContentType};
|
||||
use database::mungos::by_id::find_one_by_id;
|
||||
use derive_variants::{EnumVariants, ExtractVariant};
|
||||
use formatting::format_serror;
|
||||
use futures::{
|
||||
use futures_util::{
|
||||
StreamExt as _, future::join_all, stream::FuturesUnordered,
|
||||
};
|
||||
use komodo_client::{
|
||||
|
||||
@@ -6,7 +6,7 @@ use database::mungos::{
|
||||
find::find_collect,
|
||||
mongodb::{bson::doc, options::FindOptions},
|
||||
};
|
||||
use futures::TryStreamExt;
|
||||
use futures_util::TryStreamExt;
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
|
||||
@@ -114,6 +114,7 @@ enum ReadRequest {
|
||||
ListServers(ListServers),
|
||||
ListFullServers(ListFullServers),
|
||||
ListTerminals(ListTerminals),
|
||||
ListAllTerminals(ListAllTerminals),
|
||||
|
||||
// ==== DOCKER ====
|
||||
GetDockerContainersSummary(GetDockerContainersSummary),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use futures::future::join_all;
|
||||
use futures_util::future::join_all;
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
|
||||
@@ -12,6 +12,7 @@ use database::mungos::{
|
||||
find::find_collect,
|
||||
mongodb::{bson::doc, options::FindOptions},
|
||||
};
|
||||
use futures_util::{StreamExt as _, stream::FuturesUnordered};
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
@@ -29,7 +30,7 @@ use komodo_client::{
|
||||
permission::PermissionLevel,
|
||||
server::{
|
||||
Server, ServerActionState, ServerListItem, ServerState,
|
||||
TerminalInfo,
|
||||
TerminalInfo, TerminalInfoWithServer,
|
||||
},
|
||||
stack::{Stack, StackServiceNames},
|
||||
stats::{SystemInformation, SystemProcess},
|
||||
@@ -869,20 +870,78 @@ impl Resolve<ReadArgs> for ListTerminals {
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?;
|
||||
let cache = terminals_cache().get_or_insert(server.id.clone());
|
||||
let mut cache = cache.lock().await;
|
||||
if self.fresh || komodo_timestamp() > cache.ttl {
|
||||
cache.list = periphery_client(&server)
|
||||
.await?
|
||||
.request(periphery_client::api::terminal::ListTerminals {
|
||||
container: None,
|
||||
})
|
||||
.await
|
||||
.context("Failed to get fresh terminal list")?;
|
||||
cache.ttl = komodo_timestamp() + TERMINAL_CACHE_TIMEOUT;
|
||||
Ok(cache.list.clone())
|
||||
list_terminals_inner(&server, self.fresh)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListAllTerminals {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &ReadArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
Ok(cache.list.clone())
|
||||
}
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
|
||||
let mut terminals = resource::list_full_for_user::<Server>(
|
||||
self.query, &args.user, &all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|server| async move {
|
||||
(
|
||||
list_terminals_inner(&server, self.fresh).await,
|
||||
(server.id, server.name),
|
||||
)
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.flat_map(|(terminals, server)| {
|
||||
let terminals = terminals.ok()?;
|
||||
Some((terminals, server))
|
||||
})
|
||||
.flat_map(|(terminals, (server_id, server_name))| {
|
||||
terminals.into_iter().map(move |info| {
|
||||
TerminalInfoWithServer::from_terminal_info(
|
||||
&server_id,
|
||||
&server_name,
|
||||
info,
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
terminals.sort_by(|a, b| {
|
||||
a.server_name.cmp(&b.server_name).then(a.name.cmp(&b.name))
|
||||
});
|
||||
|
||||
Ok(terminals)
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_terminals_inner(
|
||||
server: &Server,
|
||||
fresh: bool,
|
||||
) -> anyhow::Result<Vec<TerminalInfo>> {
|
||||
let cache = terminals_cache().get_or_insert(server.id.clone());
|
||||
let mut cache = cache.lock().await;
|
||||
if fresh || komodo_timestamp() > cache.ttl {
|
||||
cache.list = periphery_client(server)
|
||||
.await?
|
||||
.request(periphery_client::api::terminal::ListTerminals {
|
||||
container: None,
|
||||
})
|
||||
.await
|
||||
.context("Failed to get fresh terminal list")?;
|
||||
cache.ttl = komodo_timestamp() + TERMINAL_CACHE_TIMEOUT;
|
||||
Ok(cache.list.clone())
|
||||
} else {
|
||||
Ok(cache.list.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use anyhow::Context;
|
||||
use axum::{Extension, Router, middleware, routing::post};
|
||||
use komodo_client::{
|
||||
api::terminal::*,
|
||||
api::{terminal::*, write::TerminalRecreateMode},
|
||||
entities::{
|
||||
deployment::Deployment, permission::PermissionLevel,
|
||||
server::Server, stack::Stack, user::User,
|
||||
},
|
||||
};
|
||||
use periphery_client::api::terminal::CreateTerminal;
|
||||
use serror::Json;
|
||||
|
||||
use crate::{
|
||||
@@ -43,6 +44,7 @@ async fn execute_terminal(
|
||||
server,
|
||||
terminal,
|
||||
command,
|
||||
init,
|
||||
}): Json<ExecuteTerminalBody>,
|
||||
) -> serror::Result<axum::body::Body> {
|
||||
info!("/terminal/execute request | user: {}", user.username);
|
||||
@@ -54,8 +56,20 @@ async fn execute_terminal(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let stream = periphery_client(&server)
|
||||
.await?
|
||||
let periphery = periphery_client(&server).await?;
|
||||
|
||||
// Maybe init terminal.
|
||||
if let Some(InitTerminal { command, recreate }) = init {
|
||||
periphery
|
||||
.request(CreateTerminal {
|
||||
name: terminal.clone(),
|
||||
command,
|
||||
recreate,
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
let stream = periphery
|
||||
.execute_terminal(terminal, command)
|
||||
.await
|
||||
.context("Failed to execute command on periphery")?;
|
||||
@@ -100,7 +114,12 @@ async fn execute_container_exec(
|
||||
let periphery = periphery_client(&server).await?;
|
||||
|
||||
let stream = periphery
|
||||
.execute_container_exec(container, shell, command, recreate)
|
||||
.execute_container_exec(
|
||||
container,
|
||||
shell,
|
||||
command,
|
||||
recreate.unwrap_or(TerminalRecreateMode::DifferentCommand),
|
||||
)
|
||||
.await
|
||||
.context(
|
||||
"Failed to execute container exec command on periphery",
|
||||
@@ -146,7 +165,12 @@ async fn execute_deployment_exec(
|
||||
let periphery = periphery_client(&server).await?;
|
||||
|
||||
let stream = periphery
|
||||
.execute_container_exec(deployment.name, shell, command, recreate)
|
||||
.execute_container_exec(
|
||||
deployment.name,
|
||||
shell,
|
||||
command,
|
||||
recreate.unwrap_or(TerminalRecreateMode::DifferentCommand),
|
||||
)
|
||||
.await
|
||||
.context(
|
||||
"Failed to execute container exec command on periphery",
|
||||
@@ -209,7 +233,12 @@ async fn execute_stack_exec(
|
||||
let periphery = periphery_client(&server).await?;
|
||||
|
||||
let stream = periphery
|
||||
.execute_container_exec(container, shell, command, recreate)
|
||||
.execute_container_exec(
|
||||
container,
|
||||
shell,
|
||||
command,
|
||||
recreate.unwrap_or(TerminalRecreateMode::DifferentCommand),
|
||||
)
|
||||
.await
|
||||
.context(
|
||||
"Failed to execute container exec command on periphery",
|
||||
|
||||
@@ -390,23 +390,28 @@ impl Resolve<WriteArgs> for RefreshBuildCache {
|
||||
None
|
||||
};
|
||||
|
||||
let (
|
||||
remote_path,
|
||||
remote_contents,
|
||||
remote_error,
|
||||
latest_hash,
|
||||
latest_message,
|
||||
) = if build.config.files_on_host {
|
||||
let RemoteDockerfileContents {
|
||||
path,
|
||||
contents,
|
||||
error,
|
||||
hash,
|
||||
message,
|
||||
} = if build.config.files_on_host {
|
||||
// =============
|
||||
// FILES ON HOST
|
||||
// =============
|
||||
match get_on_host_dockerfile(&build).await {
|
||||
Ok(FileContents { path, contents }) => {
|
||||
(Some(path), Some(contents), None, None, None)
|
||||
}
|
||||
Err(e) => {
|
||||
(None, None, Some(format_serror(&e.into())), None, None)
|
||||
RemoteDockerfileContents {
|
||||
path: Some(path),
|
||||
contents: Some(contents),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
Err(e) => RemoteDockerfileContents {
|
||||
error: Some(format_serror(&e.into())),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
} else if let Some(repo) = &repo {
|
||||
let Some(res) = get_git_remote(&build, repo.into()).await?
|
||||
@@ -426,7 +431,7 @@ impl Resolve<WriteArgs> for RefreshBuildCache {
|
||||
// =============
|
||||
// UI BASED FILE
|
||||
// =============
|
||||
(None, None, None, None, None)
|
||||
RemoteDockerfileContents::default()
|
||||
};
|
||||
|
||||
let info = BuildInfo {
|
||||
@@ -434,11 +439,11 @@ impl Resolve<WriteArgs> for RefreshBuildCache {
|
||||
built_hash: build.info.built_hash,
|
||||
built_message: build.info.built_message,
|
||||
built_contents: build.info.built_contents,
|
||||
remote_path,
|
||||
remote_contents,
|
||||
remote_error,
|
||||
latest_hash,
|
||||
latest_message,
|
||||
remote_path: path,
|
||||
remote_contents: contents,
|
||||
remote_error: error,
|
||||
latest_hash: hash,
|
||||
latest_message: message,
|
||||
};
|
||||
|
||||
let info = to_document(&info)
|
||||
@@ -530,15 +535,7 @@ async fn get_on_host_dockerfile(
|
||||
async fn get_git_remote(
|
||||
build: &Build,
|
||||
mut clone_args: RepoExecutionArgs,
|
||||
) -> anyhow::Result<
|
||||
Option<(
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
)>,
|
||||
> {
|
||||
) -> anyhow::Result<Option<RemoteDockerfileContents>> {
|
||||
if clone_args.provider.is_empty() {
|
||||
// Nothing to do here
|
||||
return Ok(None);
|
||||
@@ -565,7 +562,17 @@ async fn get_git_remote(
|
||||
access_token,
|
||||
)
|
||||
.await
|
||||
.context("failed to clone build repo")?;
|
||||
.context("Failed to clone Build repo")?;
|
||||
|
||||
// Ensure clone / pull successful,
|
||||
// propogate error log -> 'errored' and return.
|
||||
if let Some(failure) = res.logs.iter().find(|log| !log.success) {
|
||||
return Ok(Some(RemoteDockerfileContents {
|
||||
path: Some(format!("Failed at: {}", failure.stage)),
|
||||
error: Some(failure.combined()),
|
||||
..Default::default()
|
||||
}));
|
||||
}
|
||||
|
||||
let relative_path = PathBuf::from(&build.config.build_path)
|
||||
.join(&build.config.dockerfile_path);
|
||||
@@ -578,11 +585,20 @@ async fn get_git_remote(
|
||||
Ok(contents) => (Some(contents), None),
|
||||
Err(e) => (None, Some(format_serror(&e.into()))),
|
||||
};
|
||||
Ok(Some((
|
||||
Some(relative_path.display().to_string()),
|
||||
Ok(Some(RemoteDockerfileContents {
|
||||
path: Some(relative_path.display().to_string()),
|
||||
contents,
|
||||
error,
|
||||
res.commit_hash,
|
||||
res.commit_message,
|
||||
)))
|
||||
hash: res.commit_hash,
|
||||
message: res.commit_message,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RemoteDockerfileContents {
|
||||
pub path: Option<String>,
|
||||
pub contents: Option<String>,
|
||||
pub error: Option<String>,
|
||||
pub hash: Option<String>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ pub enum WriteRequest {
|
||||
CreateTerminal(CreateTerminal),
|
||||
DeleteTerminal(DeleteTerminal),
|
||||
DeleteAllTerminals(DeleteAllTerminals),
|
||||
BatchDeleteAllTerminals(BatchDeleteAllTerminals),
|
||||
UpdateServerPublicKey(UpdateServerPublicKey),
|
||||
RotateServerKeys(RotateServerKeys),
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use anyhow::Context;
|
||||
use formatting::{bold, format_serror};
|
||||
use futures_util::{StreamExt, stream::FuturesUnordered};
|
||||
use komodo_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
@@ -16,6 +17,7 @@ use resolver_api::Resolve;
|
||||
use crate::{
|
||||
helpers::{
|
||||
periphery_client,
|
||||
query::get_all_tags,
|
||||
update::{add_update, make_update, update_update},
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
@@ -292,6 +294,59 @@ impl Resolve<WriteArgs> for DeleteAllTerminals {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<WriteArgs> for BatchDeleteAllTerminals {
|
||||
#[instrument(
|
||||
"BatchDeleteAllTerminals",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
query = format!("{:?}", self.query),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
|
||||
resource::list_full_for_user::<Server>(
|
||||
self.query, user, &all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|server| async move {
|
||||
let res = async {
|
||||
let periphery = periphery_client(&server).await?;
|
||||
|
||||
periphery
|
||||
.request(api::terminal::DeleteAllTerminals {})
|
||||
.await
|
||||
.context("Failed to delete all terminals on Periphery")?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"Failed to delete all terminals on {} ({}) | {e:#}",
|
||||
server.name, server.id
|
||||
)
|
||||
}
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<WriteArgs> for UpdateServerPublicKey {
|
||||
#[instrument(
|
||||
"UpdateServerPublicKey",
|
||||
|
||||
@@ -257,7 +257,8 @@ impl<'a> From<&'a OwnedPeripheryConnectionArgs>
|
||||
pub type ResponseChannels =
|
||||
CloneCache<Uuid, Sender<EncodedResponse<EncodedJsonMessage>>>;
|
||||
|
||||
pub type TerminalChannels = CloneCache<Uuid, Sender<Vec<u8>>>;
|
||||
pub type TerminalChannels =
|
||||
CloneCache<Uuid, Sender<anyhow::Result<Vec<u8>>>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PeripheryConnection {
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::time::{Duration, Instant};
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mungos::by_id::find_one_by_id;
|
||||
use formatting::{Color, bold, colored, format_serror, muted};
|
||||
use futures::future::join_all;
|
||||
use futures_util::future::join_all;
|
||||
use komodo_client::{
|
||||
api::execute::*,
|
||||
entities::{
|
||||
|
||||
@@ -3,7 +3,7 @@ use async_timing_util::{
|
||||
ONE_DAY_MS, Timelength, unix_timestamp_ms, wait_until_timelength,
|
||||
};
|
||||
use database::mungos::{find::find_collect, mongodb::bson::doc};
|
||||
use futures::{StreamExt, stream::FuturesUnordered};
|
||||
use futures_util::{StreamExt, stream::FuturesUnordered};
|
||||
use periphery_client::api::docker::PruneImages;
|
||||
|
||||
use crate::{config::core_config, state::db_client};
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::{Arc, OnceLock};
|
||||
use async_timing_util::wait_until_timelength;
|
||||
use cache::CloneCache;
|
||||
use database::mungos::{find::find_collect, mongodb::bson::doc};
|
||||
use futures::future::join_all;
|
||||
use futures_util::future::join_all;
|
||||
use helpers::insert_stacks_status_unknown;
|
||||
use komodo_client::entities::{
|
||||
build::Build,
|
||||
|
||||
@@ -5,9 +5,8 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use bytes::Bytes;
|
||||
use cache::CloneCache;
|
||||
use futures::Stream;
|
||||
use futures_util::Stream;
|
||||
use komodo_client::api::write::TerminalRecreateMode;
|
||||
use periphery_client::{
|
||||
api::terminal::{
|
||||
@@ -16,23 +15,25 @@ use periphery_client::{
|
||||
},
|
||||
transport::EncodedTransportMessage,
|
||||
};
|
||||
use transport::channel::{Receiver, Sender, channel};
|
||||
use transport::channel::{Receiver, Sender};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
periphery::PeripheryClient, state::periphery_connections,
|
||||
};
|
||||
|
||||
pub struct ConnectTerminalResponse {
|
||||
pub channel: Uuid,
|
||||
pub sender: Sender<EncodedTransportMessage>,
|
||||
pub receiver: Receiver<anyhow::Result<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl PeripheryClient {
|
||||
#[instrument("ConnectTerminal", skip(self), fields(server_id = self.id))]
|
||||
pub async fn connect_terminal(
|
||||
&self,
|
||||
terminal: String,
|
||||
) -> anyhow::Result<(
|
||||
Uuid,
|
||||
Sender<EncodedTransportMessage>,
|
||||
Receiver<Vec<u8>>,
|
||||
)> {
|
||||
) -> anyhow::Result<ConnectTerminalResponse> {
|
||||
tracing::trace!(
|
||||
"request | type: ConnectTerminal | terminal name: {terminal}",
|
||||
);
|
||||
@@ -42,23 +43,27 @@ impl PeripheryClient {
|
||||
|| format!("No connection found for server {}", self.id),
|
||||
)?;
|
||||
|
||||
let channel_id = self
|
||||
let channel = self
|
||||
.request(ConnectTerminal { terminal })
|
||||
.await
|
||||
.context("Failed to create terminal connection")?;
|
||||
|
||||
let (sender, receiever) = channel();
|
||||
connection.terminals.insert(channel_id, sender).await;
|
||||
let (sender, receiver) = transport::channel::channel();
|
||||
connection.terminals.insert(channel, sender).await;
|
||||
|
||||
connection
|
||||
.sender
|
||||
.send_terminal(channel_id, Bytes::new())
|
||||
.send_terminal(channel, Ok(Vec::with_capacity(17))) // 16 bytes uuid + 1 EncodedResponse
|
||||
.await
|
||||
.context(
|
||||
"Failed to send TerminalTrigger to begin forwarding.",
|
||||
)?;
|
||||
|
||||
Ok((channel_id, connection.sender.clone(), receiever))
|
||||
Ok(ConnectTerminalResponse {
|
||||
channel,
|
||||
sender: connection.sender.clone(),
|
||||
receiver,
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument("ConnectContainerExec", skip(self), fields(server_id = self.id))]
|
||||
@@ -67,11 +72,7 @@ impl PeripheryClient {
|
||||
container: String,
|
||||
shell: String,
|
||||
recreate: TerminalRecreateMode,
|
||||
) -> anyhow::Result<(
|
||||
Uuid,
|
||||
Sender<EncodedTransportMessage>,
|
||||
Receiver<Vec<u8>>,
|
||||
)> {
|
||||
) -> anyhow::Result<ConnectTerminalResponse> {
|
||||
tracing::trace!(
|
||||
"request | type: ConnectContainerExec | container name: {container} | shell: {shell}",
|
||||
);
|
||||
@@ -81,7 +82,7 @@ impl PeripheryClient {
|
||||
|| format!("No connection found for server {}", self.id),
|
||||
)?;
|
||||
|
||||
let channel_id = self
|
||||
let channel = self
|
||||
.request(ConnectContainerExec {
|
||||
container,
|
||||
shell,
|
||||
@@ -90,18 +91,22 @@ impl PeripheryClient {
|
||||
.await
|
||||
.context("Failed to create container exec connection")?;
|
||||
|
||||
let (sender, receiever) = channel();
|
||||
connection.terminals.insert(channel_id, sender).await;
|
||||
let (sender, receiver) = transport::channel::channel();
|
||||
connection.terminals.insert(channel, sender).await;
|
||||
|
||||
connection
|
||||
.sender
|
||||
.send_terminal(channel_id, Bytes::new())
|
||||
.send_terminal(channel, Ok(Vec::with_capacity(17)))
|
||||
.await
|
||||
.context(
|
||||
"Failed to send TerminalTrigger to begin forwarding.",
|
||||
)?;
|
||||
|
||||
Ok((channel_id, connection.sender.clone(), receiever))
|
||||
Ok(ConnectTerminalResponse {
|
||||
channel,
|
||||
sender: connection.sender.clone(),
|
||||
receiver,
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument("ConnectContainerAttach", skip(self), fields(server_id = self.id))]
|
||||
@@ -109,11 +114,7 @@ impl PeripheryClient {
|
||||
&self,
|
||||
container: String,
|
||||
recreate: TerminalRecreateMode,
|
||||
) -> anyhow::Result<(
|
||||
Uuid,
|
||||
Sender<EncodedTransportMessage>,
|
||||
Receiver<Vec<u8>>,
|
||||
)> {
|
||||
) -> anyhow::Result<ConnectTerminalResponse> {
|
||||
tracing::trace!(
|
||||
"request | type: ConnectContainerAttach | container name: {container}",
|
||||
);
|
||||
@@ -131,18 +132,22 @@ impl PeripheryClient {
|
||||
.await
|
||||
.context("Failed to create container attach connection")?;
|
||||
|
||||
let (sender, receiever) = transport::channel::channel();
|
||||
let (sender, receiver) = transport::channel::channel();
|
||||
connection.terminals.insert(channel, sender).await;
|
||||
|
||||
connection
|
||||
.sender
|
||||
.send_terminal(channel, Bytes::new())
|
||||
.send_terminal(channel, Ok(Vec::with_capacity(17)))
|
||||
.await
|
||||
.context(
|
||||
"Failed to send TerminalTrigger to begin forwarding.",
|
||||
)?;
|
||||
|
||||
Ok((channel, connection.sender.clone(), receiever))
|
||||
Ok(ConnectTerminalResponse {
|
||||
channel,
|
||||
sender: connection.sender.clone(),
|
||||
receiver,
|
||||
})
|
||||
}
|
||||
|
||||
/// Executes command on specified terminal,
|
||||
@@ -175,27 +180,25 @@ impl PeripheryClient {
|
||||
|| format!("No connection found for server {}", self.id),
|
||||
)?;
|
||||
|
||||
let channel_id = self
|
||||
let channel = self
|
||||
.request(ExecuteTerminal { terminal, command })
|
||||
.await
|
||||
.context("Failed to create execute terminal connection")?;
|
||||
|
||||
let (terminal_sender, terminal_receiver) = channel();
|
||||
connection
|
||||
.terminals
|
||||
.insert(channel_id, terminal_sender)
|
||||
.await;
|
||||
let (terminal_sender, terminal_receiver) =
|
||||
transport::channel::channel();
|
||||
connection.terminals.insert(channel, terminal_sender).await;
|
||||
|
||||
connection
|
||||
.sender
|
||||
.send_terminal(channel_id, Bytes::new())
|
||||
.send_terminal(channel, Ok(Vec::with_capacity(17)))
|
||||
.await
|
||||
.context(
|
||||
"Failed to send TerminalTrigger to begin forwarding.",
|
||||
)?;
|
||||
|
||||
Ok(ReceiverStream {
|
||||
channel_id,
|
||||
channel,
|
||||
receiver: terminal_receiver,
|
||||
channels: connection.terminals.clone(),
|
||||
})
|
||||
@@ -231,7 +234,7 @@ impl PeripheryClient {
|
||||
|| format!("No connection found for server {}", self.id),
|
||||
)?;
|
||||
|
||||
let channel_id = self
|
||||
let channel = self
|
||||
.request(ExecuteContainerExec {
|
||||
container,
|
||||
shell,
|
||||
@@ -241,21 +244,19 @@ impl PeripheryClient {
|
||||
.await
|
||||
.context("Failed to create execute terminal connection")?;
|
||||
|
||||
let (terminal_sender, terminal_receiver) = channel();
|
||||
connection
|
||||
.terminals
|
||||
.insert(channel_id, terminal_sender)
|
||||
.await;
|
||||
let (terminal_sender, terminal_receiver) =
|
||||
transport::channel::channel();
|
||||
connection.terminals.insert(channel, terminal_sender).await;
|
||||
|
||||
// Trigger forwarding to begin now that forwarding channel is ready.
|
||||
// This is required to not miss messages.
|
||||
connection
|
||||
.sender
|
||||
.send_terminal(channel_id, Bytes::new())
|
||||
.send_terminal(channel, Ok(Vec::with_capacity(17)))
|
||||
.await?;
|
||||
|
||||
Ok(ReceiverStream {
|
||||
channel_id,
|
||||
channel,
|
||||
receiver: terminal_receiver,
|
||||
channels: connection.terminals.clone(),
|
||||
})
|
||||
@@ -263,9 +264,9 @@ impl PeripheryClient {
|
||||
}
|
||||
|
||||
pub struct ReceiverStream {
|
||||
channel_id: Uuid,
|
||||
channels: Arc<CloneCache<Uuid, Sender<Vec<u8>>>>,
|
||||
receiver: Receiver<Vec<u8>>,
|
||||
channel: Uuid,
|
||||
channels: Arc<CloneCache<Uuid, Sender<anyhow::Result<Vec<u8>>>>>,
|
||||
receiver: Receiver<anyhow::Result<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl Stream for ReceiverStream {
|
||||
@@ -275,14 +276,14 @@ impl Stream for ReceiverStream {
|
||||
cx: &mut task::Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
match self.receiver.poll_recv(cx) {
|
||||
Poll::Ready(Some(bytes))
|
||||
Poll::Ready(Some(Ok(bytes)))
|
||||
if bytes == END_OF_OUTPUT.as_bytes() =>
|
||||
{
|
||||
self.cleanup();
|
||||
Poll::Ready(None)
|
||||
}
|
||||
Poll::Ready(Some(bytes)) => Poll::Ready(Some(Ok(bytes))),
|
||||
// Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
|
||||
Poll::Ready(Some(Ok(bytes))) => Poll::Ready(Some(Ok(bytes))),
|
||||
Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
|
||||
Poll::Ready(None) => {
|
||||
self.cleanup();
|
||||
Poll::Ready(None)
|
||||
@@ -296,9 +297,9 @@ impl ReceiverStream {
|
||||
fn cleanup(&self) {
|
||||
// Not the prettiest but it should be fine
|
||||
let channels = self.channels.clone();
|
||||
let id = self.channel_id;
|
||||
let channel = self.channel;
|
||||
tokio::spawn(async move {
|
||||
channels.remove(&id).await;
|
||||
channels.remove(&channel).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::collections::HashSet;
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mongo_indexed::doc;
|
||||
use database::mungos::find::find_collect;
|
||||
use futures::{FutureExt, future::BoxFuture};
|
||||
use futures_util::{FutureExt, future::BoxFuture};
|
||||
use indexmap::IndexSet;
|
||||
use komodo_client::{
|
||||
api::read::GetPermission,
|
||||
|
||||
@@ -14,7 +14,7 @@ use database::mungos::{
|
||||
},
|
||||
};
|
||||
use formatting::format_serror;
|
||||
use futures::future::join_all;
|
||||
use futures_util::future::join_all;
|
||||
use indexmap::IndexSet;
|
||||
use komodo_client::{
|
||||
api::{read::ExportResourcesToToml, write::CreateTag},
|
||||
|
||||
@@ -5,7 +5,7 @@ use database::mungos::{
|
||||
find::find_collect,
|
||||
mongodb::{Collection, bson::doc, options::FindOneOptions},
|
||||
};
|
||||
use futures::{TryStreamExt, stream::FuturesUnordered};
|
||||
use futures_util::{TryStreamExt, stream::FuturesUnordered};
|
||||
use komodo_client::{
|
||||
api::execute::Execution,
|
||||
entities::{
|
||||
|
||||
@@ -71,6 +71,7 @@ impl super::KomodoResource for Server {
|
||||
let (
|
||||
version,
|
||||
public_key,
|
||||
public_ip,
|
||||
terminals_disabled,
|
||||
container_terminals_disabled,
|
||||
) = match status.as_ref().and_then(|s| s.periphery_info.as_ref())
|
||||
@@ -78,10 +79,11 @@ impl super::KomodoResource for Server {
|
||||
Some(info) => (
|
||||
Some(info.version.clone()),
|
||||
Some(info.public_key.clone()),
|
||||
info.public_ip.clone(),
|
||||
info.terminals_disabled,
|
||||
info.container_terminals_disabled,
|
||||
),
|
||||
None => (None, None, true, true),
|
||||
None => (None, None, None, true, true),
|
||||
};
|
||||
ServerListItem {
|
||||
name: server.name,
|
||||
@@ -110,6 +112,7 @@ impl super::KomodoResource for Server {
|
||||
attempted_public_key: optional_string(
|
||||
server.info.attempted_public_key,
|
||||
),
|
||||
public_ip,
|
||||
terminals_disabled,
|
||||
container_terminals_disabled,
|
||||
},
|
||||
|
||||
@@ -11,12 +11,13 @@ use komodo_client::entities::{
|
||||
|
||||
use crate::{config::core_config, helpers::git_token};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RemoteComposeContents {
|
||||
pub successful: Vec<StackRemoteFileContents>,
|
||||
pub errored: Vec<FileContents>,
|
||||
pub hash: Option<String>,
|
||||
pub message: Option<String>,
|
||||
pub _logs: Vec<Log>,
|
||||
// pub logs: Vec<Log>,
|
||||
}
|
||||
|
||||
/// Returns Result<(read paths, error paths, logs, short hash, commit message)>
|
||||
@@ -28,11 +29,23 @@ pub async fn get_repo_compose_contents(
|
||||
) -> anyhow::Result<RemoteComposeContents> {
|
||||
let clone_args: RepoExecutionArgs =
|
||||
repo.map(Into::into).unwrap_or(stack.into());
|
||||
let (repo_path, _logs, hash, message) =
|
||||
let (repo_path, logs, hash, message) =
|
||||
ensure_remote_repo(clone_args)
|
||||
.await
|
||||
.context("Failed to clone stack repo")?;
|
||||
|
||||
// Ensure clone / pull successful,
|
||||
// propogate error log -> 'errored' and return.
|
||||
if let Some(failure) = logs.iter().find(|log| !log.success) {
|
||||
return Ok(RemoteComposeContents {
|
||||
errored: vec![FileContents {
|
||||
path: format!("Failed at: {}", failure.stage),
|
||||
contents: failure.combined(),
|
||||
}],
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
let run_directory = repo_path.join(&stack.config.run_directory);
|
||||
// This will remove any intermediate '/./' which can be a problem for some OS.
|
||||
let run_directory = run_directory.components().collect::<PathBuf>();
|
||||
@@ -69,7 +82,6 @@ pub async fn get_repo_compose_contents(
|
||||
errored,
|
||||
hash,
|
||||
message,
|
||||
_logs,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use database::mungos::{
|
||||
Document, doc, oid::ObjectId, to_bson, to_document,
|
||||
},
|
||||
};
|
||||
use futures::future::join_all;
|
||||
use futures_util::future::join_all;
|
||||
use komodo_client::{
|
||||
api::{
|
||||
auth::SignUpLocalUser,
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::{collections::HashMap, time::Duration};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use formatting::{Color, bold, colored, format_serror, muted};
|
||||
use futures::future::join_all;
|
||||
use futures_util::future::join_all;
|
||||
use komodo_client::{
|
||||
api::{
|
||||
execute::{Deploy, DeployStack},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::Context;
|
||||
use anyhow::{Context, anyhow};
|
||||
use komodo_client::entities::{
|
||||
RepoExecutionArgs, RepoExecutionResponse,
|
||||
repo::Repo,
|
||||
@@ -99,9 +99,22 @@ async fn get_repo(
|
||||
format!("Failed to update resource repo at {repo_path:?}")
|
||||
})?;
|
||||
|
||||
// let hash = hash.context("failed to get commit hash")?;
|
||||
// let message =
|
||||
// message.context("failed to get commit hash message")?;
|
||||
// Ensure clone / pull successful,
|
||||
// propogate error log -> 'errored' and return.
|
||||
if let Some(failure) = logs.iter().find(|log| !log.success) {
|
||||
return Ok(RemoteResources {
|
||||
resources: Err(anyhow!("Repo clone / pull failed")),
|
||||
files: Vec::new(),
|
||||
file_errors: vec![SyncFileContents {
|
||||
resource_path: String::from("Repo error"),
|
||||
path: format!("Failed at: {}", failure.stage),
|
||||
contents: failure.combined(),
|
||||
}],
|
||||
logs,
|
||||
hash: None,
|
||||
message: None,
|
||||
});
|
||||
}
|
||||
|
||||
let (mut files, mut file_errors) = (Vec::new(), Vec::new());
|
||||
let resources = super::file::read_resources(
|
||||
|
||||
@@ -2,10 +2,13 @@ use axum::{
|
||||
extract::{Query, WebSocketUpgrade, ws::Message},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use futures::SinkExt;
|
||||
use futures_util::SinkExt;
|
||||
use komodo_client::{
|
||||
api::terminal::{
|
||||
ConnectContainerAttachQuery, ConnectContainerExecQuery,
|
||||
api::{
|
||||
terminal::{
|
||||
ConnectContainerAttachQuery, ConnectContainerExecQuery,
|
||||
},
|
||||
write::TerminalRecreateMode,
|
||||
},
|
||||
entities::{permission::PermissionLevel, server::Server},
|
||||
};
|
||||
@@ -52,7 +55,7 @@ pub async fn exec(
|
||||
&server,
|
||||
container,
|
||||
shell,
|
||||
recreate,
|
||||
recreate.unwrap_or(TerminalRecreateMode::DifferentCommand),
|
||||
)
|
||||
.await
|
||||
})
|
||||
@@ -96,7 +99,7 @@ pub async fn attach(
|
||||
client_socket,
|
||||
&server,
|
||||
container,
|
||||
recreate,
|
||||
recreate.unwrap_or(TerminalRecreateMode::DifferentCommand),
|
||||
)
|
||||
.await
|
||||
})
|
||||
|
||||
@@ -2,10 +2,13 @@ use axum::{
|
||||
extract::{Query, WebSocketUpgrade, ws::Message},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use futures::SinkExt;
|
||||
use futures_util::SinkExt;
|
||||
use komodo_client::{
|
||||
api::terminal::{
|
||||
ConnectDeploymentAttachQuery, ConnectDeploymentExecQuery,
|
||||
api::{
|
||||
terminal::{
|
||||
ConnectDeploymentAttachQuery, ConnectDeploymentExecQuery,
|
||||
},
|
||||
write::TerminalRecreateMode,
|
||||
},
|
||||
entities::{
|
||||
deployment::Deployment, permission::PermissionLevel,
|
||||
@@ -67,7 +70,7 @@ pub async fn exec(
|
||||
&server,
|
||||
deployment.name,
|
||||
shell,
|
||||
recreate,
|
||||
recreate.unwrap_or(TerminalRecreateMode::DifferentCommand),
|
||||
)
|
||||
.await
|
||||
})
|
||||
@@ -123,7 +126,7 @@ pub async fn attach(
|
||||
client_socket,
|
||||
&server,
|
||||
deployment.name,
|
||||
recreate,
|
||||
recreate.unwrap_or(TerminalRecreateMode::DifferentCommand),
|
||||
)
|
||||
.await
|
||||
})
|
||||
|
||||
+82
-79
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
auth::{auth_api_key_check_enabled, auth_jwt_check_enabled},
|
||||
helpers::query::get_user,
|
||||
periphery::PeripheryClient,
|
||||
periphery::{PeripheryClient, terminal::ConnectTerminalResponse},
|
||||
state::periphery_connections,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
@@ -11,19 +11,14 @@ use axum::{
|
||||
routing::get,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use komodo_client::{
|
||||
api::write::TerminalRecreateMode,
|
||||
entities::{server::Server, user::User},
|
||||
ws::WsLoginMessage,
|
||||
};
|
||||
use periphery_client::{
|
||||
api::terminal::DisconnectTerminal,
|
||||
transport::EncodedTransportMessage,
|
||||
};
|
||||
use periphery_client::api::terminal::DisconnectTerminal;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use transport::channel::{Receiver, Sender};
|
||||
use uuid::Uuid;
|
||||
|
||||
mod container;
|
||||
mod deployment;
|
||||
@@ -157,34 +152,26 @@ async fn handle_container_exec_terminal(
|
||||
|
||||
trace!("connecting to periphery container exec websocket");
|
||||
|
||||
let (periphery_connection_id, periphery_sender, periphery_receiver) =
|
||||
match periphery
|
||||
.connect_container_exec(container, shell, recreate)
|
||||
.await
|
||||
{
|
||||
Ok(ws) => ws,
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"Failed connect to periphery container exec websocket | {e:#}"
|
||||
);
|
||||
let _ = client_socket
|
||||
.send(ws::Message::text(format!("ERROR: {e:#}")))
|
||||
.await;
|
||||
let _ = client_socket.close().await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let response = match periphery
|
||||
.connect_container_exec(container, shell, recreate)
|
||||
.await
|
||||
{
|
||||
Ok(ws) => ws,
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"Failed connect to periphery container exec websocket | {e:#}"
|
||||
);
|
||||
let _ = client_socket
|
||||
.send(ws::Message::text(format!("ERROR: {e:#}")))
|
||||
.await;
|
||||
let _ = client_socket.close().await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
trace!("connected to periphery container exec websocket");
|
||||
|
||||
forward_ws_channel(
|
||||
periphery,
|
||||
client_socket,
|
||||
periphery_connection_id,
|
||||
periphery_sender,
|
||||
periphery_receiver,
|
||||
)
|
||||
.await
|
||||
forward_ws_channel(periphery, client_socket, response).await
|
||||
}
|
||||
|
||||
async fn handle_container_attach_terminal(
|
||||
@@ -208,42 +195,36 @@ async fn handle_container_attach_terminal(
|
||||
|
||||
trace!("connecting to periphery container exec websocket");
|
||||
|
||||
let (periphery_connection_id, periphery_sender, periphery_receiver) =
|
||||
match periphery
|
||||
.connect_container_attach(container, recreate)
|
||||
.await
|
||||
{
|
||||
Ok(ws) => ws,
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"Failed connect to periphery container attach websocket | {e:#}"
|
||||
);
|
||||
let _ = client_socket
|
||||
.send(ws::Message::text(format!("ERROR: {e:#}")))
|
||||
.await;
|
||||
let _ = client_socket.close().await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let response = match periphery
|
||||
.connect_container_attach(container, recreate)
|
||||
.await
|
||||
{
|
||||
Ok(ws) => ws,
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"Failed connect to periphery container attach websocket | {e:#}"
|
||||
);
|
||||
let _ = client_socket
|
||||
.send(ws::Message::text(format!("ERROR: {e:#}")))
|
||||
.await;
|
||||
let _ = client_socket.close().await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
trace!("connected to periphery container attach websocket");
|
||||
|
||||
forward_ws_channel(
|
||||
periphery,
|
||||
client_socket,
|
||||
periphery_connection_id,
|
||||
periphery_sender,
|
||||
periphery_receiver,
|
||||
)
|
||||
.await
|
||||
forward_ws_channel(periphery, client_socket, response).await
|
||||
}
|
||||
|
||||
async fn forward_ws_channel(
|
||||
periphery: PeripheryClient,
|
||||
client_socket: axum::extract::ws::WebSocket,
|
||||
periphery_connection_id: Uuid,
|
||||
periphery_sender: Sender<EncodedTransportMessage>,
|
||||
mut periphery_receiver: Receiver<Vec<u8>>,
|
||||
ConnectTerminalResponse {
|
||||
channel,
|
||||
sender: periphery_sender,
|
||||
receiver: mut periphery_receiver,
|
||||
}: ConnectTerminalResponse,
|
||||
) {
|
||||
let (mut client_send, mut client_receive) = client_socket.split();
|
||||
let cancel = CancellationToken::new();
|
||||
@@ -264,7 +245,7 @@ async fn forward_ws_channel(
|
||||
match client_recv_res {
|
||||
Some(Ok(ws::Message::Binary(bytes))) => {
|
||||
if let Err(e) = periphery_sender
|
||||
.send_terminal(periphery_connection_id, bytes)
|
||||
.send_terminal(channel, Ok(bytes.into()))
|
||||
.await
|
||||
{
|
||||
debug!("Failed to send terminal message | {e:?}",);
|
||||
@@ -275,7 +256,7 @@ async fn forward_ws_channel(
|
||||
Some(Ok(ws::Message::Text(text))) => {
|
||||
let bytes: Bytes = text.into();
|
||||
if let Err(e) = periphery_sender
|
||||
.send_terminal(periphery_connection_id, bytes)
|
||||
.send_terminal(channel, Ok(bytes.into()))
|
||||
.await
|
||||
{
|
||||
debug!("Failed to send terminal message | {e:?}",);
|
||||
@@ -283,21 +264,38 @@ async fn forward_ws_channel(
|
||||
break;
|
||||
};
|
||||
}
|
||||
// TODO: Disconnect from periphery when client disconnects
|
||||
Some(Ok(ws::Message::Close(_frame))) => {
|
||||
let _ = periphery_sender
|
||||
.send_terminal(
|
||||
channel,
|
||||
Err(anyhow!("Client disconnected")),
|
||||
)
|
||||
.await;
|
||||
cancel.cancel();
|
||||
break;
|
||||
}
|
||||
Some(Err(_e)) => {
|
||||
let _ = periphery_sender
|
||||
.send_terminal(
|
||||
channel,
|
||||
Err(anyhow!("Client disconnected")),
|
||||
)
|
||||
.await;
|
||||
cancel.cancel();
|
||||
break;
|
||||
}
|
||||
None => {
|
||||
let _ = periphery_sender
|
||||
.send_terminal(
|
||||
channel,
|
||||
Err(anyhow!("Client disconnected")),
|
||||
)
|
||||
.await;
|
||||
cancel.cancel();
|
||||
break;
|
||||
}
|
||||
// Ignore
|
||||
Some(Ok(_)) => {}
|
||||
Some(Err(_e)) => {
|
||||
cancel.cancel();
|
||||
break;
|
||||
}
|
||||
None => {
|
||||
cancel.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -306,7 +304,7 @@ async fn forward_ws_channel(
|
||||
loop {
|
||||
// Already adheres to cancellation token
|
||||
match periphery_receiver.recv().await {
|
||||
Ok(bytes) => {
|
||||
Ok(Ok(bytes)) => {
|
||||
if let Err(e) =
|
||||
client_send.send(ws::Message::Binary(bytes.into())).await
|
||||
{
|
||||
@@ -315,6 +313,14 @@ async fn forward_ws_channel(
|
||||
break;
|
||||
};
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
let _ = client_send
|
||||
.send(ws::Message::Text(format!("{e:#}").into()))
|
||||
.await;
|
||||
let _ = client_send.close().await;
|
||||
cancel.cancel();
|
||||
break;
|
||||
}
|
||||
Err(_) => {
|
||||
let _ =
|
||||
client_send.send(ws::Message::text("STREAM EOF")).await;
|
||||
@@ -328,11 +334,8 @@ async fn forward_ws_channel(
|
||||
tokio::join!(core_to_periphery, periphery_to_core);
|
||||
|
||||
// Cleanup
|
||||
if let Err(e) = periphery
|
||||
.request(DisconnectTerminal {
|
||||
id: periphery_connection_id,
|
||||
})
|
||||
.await
|
||||
if let Err(e) =
|
||||
periphery.request(DisconnectTerminal { channel }).await
|
||||
{
|
||||
warn!(
|
||||
"Failed to disconnect Periphery terminal forwarding | {e:#}",
|
||||
@@ -341,6 +344,6 @@ async fn forward_ws_channel(
|
||||
if let Some(connection) =
|
||||
periphery_connections().get(&periphery.id).await
|
||||
{
|
||||
connection.terminals.remove(&periphery_connection_id).await;
|
||||
connection.terminals.remove(&channel).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@ use axum::{
|
||||
extract::{Query, WebSocketUpgrade, ws::Message},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use futures::SinkExt;
|
||||
use futures_util::SinkExt;
|
||||
use komodo_client::{
|
||||
api::terminal::{ConnectStackAttachQuery, ConnectStackExecQuery},
|
||||
api::{
|
||||
terminal::{ConnectStackAttachQuery, ConnectStackExecQuery},
|
||||
write::TerminalRecreateMode,
|
||||
},
|
||||
entities::{
|
||||
permission::PermissionLevel, server::Server, stack::Stack,
|
||||
},
|
||||
@@ -37,7 +40,7 @@ pub async fn exec(
|
||||
&server,
|
||||
container,
|
||||
shell,
|
||||
recreate,
|
||||
recreate.unwrap_or(TerminalRecreateMode::DifferentCommand),
|
||||
)
|
||||
.await
|
||||
})
|
||||
@@ -63,7 +66,7 @@ pub async fn attach(
|
||||
client_socket,
|
||||
&server,
|
||||
container,
|
||||
recreate,
|
||||
recreate.unwrap_or(TerminalRecreateMode::DifferentCommand),
|
||||
)
|
||||
.await
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ use axum::{
|
||||
extract::{Query, WebSocketUpgrade, ws::Message},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use futures::SinkExt;
|
||||
use futures_util::SinkExt;
|
||||
use komodo_client::{
|
||||
api::terminal::ConnectTerminalQuery,
|
||||
entities::{permission::PermissionLevel, server::Server},
|
||||
@@ -59,11 +59,7 @@ pub async fn handler(
|
||||
|
||||
trace!("connecting to periphery terminal websocket");
|
||||
|
||||
let (
|
||||
periphery_connection_id,
|
||||
periphery_sender,
|
||||
periphery_receiver,
|
||||
) = match periphery.connect_terminal(terminal).await {
|
||||
let response = match periphery.connect_terminal(terminal).await {
|
||||
Ok(ws) => ws,
|
||||
Err(e) => {
|
||||
debug!("Failed connect to periphery terminal | {e:#}");
|
||||
@@ -77,13 +73,6 @@ pub async fn handler(
|
||||
|
||||
trace!("connected to periphery terminal websocket");
|
||||
|
||||
forward_ws_channel(
|
||||
periphery,
|
||||
client_socket,
|
||||
periphery_connection_id,
|
||||
periphery_sender,
|
||||
periphery_receiver,
|
||||
)
|
||||
.await
|
||||
forward_ws_channel(periphery, client_socket, response).await
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use axum::{
|
||||
extract::{WebSocketUpgrade, ws::Message},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use komodo_client::entities::{
|
||||
ResourceTarget, permission::PermissionLevel, user::User,
|
||||
};
|
||||
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
## Update certificates.
|
||||
update-ca-certificates
|
||||
|
||||
## Let the actual command take over
|
||||
exec "$@"
|
||||
@@ -37,7 +37,9 @@ derive_variants.workspace = true
|
||||
resolver_api.workspace = true
|
||||
run_command.workspace = true
|
||||
# external
|
||||
hickory-resolver.workspace = true
|
||||
serde_yaml_ng.workspace = true
|
||||
futures-util.workspace = true
|
||||
tokio-stream.workspace = true
|
||||
portable-pty.workspace = true
|
||||
shell-escape.workspace = true
|
||||
@@ -47,7 +49,6 @@ serde_json.workspace = true
|
||||
tokio-util.workspace = true
|
||||
arc-swap.workspace = true
|
||||
colored.workspace = true
|
||||
futures.workspace = true
|
||||
tracing.workspace = true
|
||||
bollard.workspace = true
|
||||
sysinfo.workspace = true
|
||||
|
||||
@@ -22,6 +22,9 @@ RUN sh ./debian-deps.sh && rm ./debian-deps.sh
|
||||
|
||||
COPY --from=builder /builder/target/release/periphery /usr/local/bin/periphery
|
||||
|
||||
COPY ./bin/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
EXPOSE 8120
|
||||
|
||||
# Can mount config file to /config/*config*.toml and it will be picked up.
|
||||
@@ -29,7 +32,8 @@ ENV PERIPHERY_CONFIG_PATHS="/config"
|
||||
# Change the default in container to /config/keys to match Core
|
||||
ENV PERIPHERY_PRIVATE_KEY="file:/config/keys/periphery.key"
|
||||
|
||||
CMD [ "/bin/bash", "-c", "update-ca-certificates && periphery" ]
|
||||
ENTRYPOINT [ "entrypoint.sh" ]
|
||||
CMD [ "periphery" ]
|
||||
|
||||
# Label to prevent Komodo from stopping with StopAllContainers
|
||||
LABEL komodo.skip="true"
|
||||
|
||||
@@ -25,6 +25,9 @@ COPY --from=aarch64 /periphery /app/arch/linux/arm64
|
||||
ARG TARGETPLATFORM
|
||||
RUN mv /app/arch/${TARGETPLATFORM} /usr/local/bin/periphery && rm -r /app/arch
|
||||
|
||||
COPY ./bin/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
EXPOSE 8120
|
||||
|
||||
# Can mount config file to /config/*config*.toml and it will be picked up.
|
||||
@@ -32,7 +35,8 @@ ENV PERIPHERY_CONFIG_PATHS="/config"
|
||||
# Change the default in container to /config/keys to match Core
|
||||
ENV PERIPHERY_PRIVATE_KEY="file:/config/keys/periphery.key"
|
||||
|
||||
CMD [ "/bin/bash", "-c", "update-ca-certificates && periphery" ]
|
||||
ENTRYPOINT [ "entrypoint.sh" ]
|
||||
CMD [ "periphery" ]
|
||||
|
||||
# Label to prevent Komodo from stopping with StopAllContainers
|
||||
LABEL komodo.skip="true"
|
||||
|
||||
@@ -14,6 +14,9 @@ RUN sh ./debian-deps.sh && rm ./debian-deps.sh
|
||||
|
||||
COPY --from=binaries /periphery /usr/local/bin/periphery
|
||||
|
||||
COPY ./bin/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
EXPOSE 8120
|
||||
|
||||
# Can mount config file to /config/*config*.toml and it will be picked up.
|
||||
@@ -21,7 +24,8 @@ ENV PERIPHERY_CONFIG_PATHS="/config"
|
||||
# Change the default in container to /config/keys to match Core
|
||||
ENV PERIPHERY_PRIVATE_KEY="file:/config/keys/periphery.key"
|
||||
|
||||
CMD [ "/bin/bash", "-c", "update-ca-certificates && periphery" ]
|
||||
ENTRYPOINT [ "entrypoint.sh" ]
|
||||
CMD [ "periphery" ]
|
||||
|
||||
# Label to prevent Komodo from stopping with StopAllContainers
|
||||
LABEL komodo.skip="true"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::Context;
|
||||
use command::run_komodo_command;
|
||||
use futures::future::join_all;
|
||||
use futures_util::future::join_all;
|
||||
use komodo_client::entities::{
|
||||
docker::{
|
||||
container::{Container, ContainerListItem, ContainerStats},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use command::run_komodo_command;
|
||||
use derive_variants::EnumVariants;
|
||||
use encoding::{EncodedJsonMessage, EncodedResponse};
|
||||
use futures::FutureExt;
|
||||
use futures_util::FutureExt;
|
||||
use komodo_client::entities::{
|
||||
config::{DockerRegistry, GitProvider},
|
||||
server::PeripheryInformation,
|
||||
@@ -19,7 +19,9 @@ use uuid::Uuid;
|
||||
use crate::{
|
||||
api::compose::list_compose_projects,
|
||||
config::periphery_config,
|
||||
state::{docker_client, periphery_keys, stats_client},
|
||||
state::{
|
||||
docker_client, host_public_ip, periphery_keys, stats_client,
|
||||
},
|
||||
};
|
||||
|
||||
pub mod terminal;
|
||||
@@ -240,6 +242,7 @@ impl Resolve<Args> for PollStatus {
|
||||
.disable_container_terminals,
|
||||
stats_polling_rate: config.stats_polling_rate,
|
||||
docker_connected: docker_client().load().is_some(),
|
||||
public_ip: host_public_ip().await.cloned(),
|
||||
},
|
||||
system_info: stats_client.info.clone(),
|
||||
system_stats,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use futures::{Stream, StreamExt, TryStreamExt};
|
||||
use colored::Colorize;
|
||||
use futures_util::{Stream, StreamExt, TryStreamExt};
|
||||
use komodo_client::entities::{
|
||||
ContainerTerminalMode, KOMODO_EXIT_CODE, NoData,
|
||||
server::TerminalInfo,
|
||||
@@ -181,7 +182,7 @@ impl Resolve<super::Args> for ConnectContainerExec {
|
||||
// Create (recreate if shell changed)
|
||||
let terminal = create_terminal(
|
||||
container.clone(),
|
||||
format!("docker exec -it {container} {shell}"),
|
||||
Some(format!("docker exec -it {container} {shell}")),
|
||||
recreate,
|
||||
Some((container, ContainerTerminalMode::Exec)),
|
||||
)
|
||||
@@ -234,7 +235,7 @@ impl Resolve<super::Args> for ConnectContainerAttach {
|
||||
// Create (recreate if shell changed)
|
||||
let terminal = create_terminal(
|
||||
container.clone(),
|
||||
format!("docker attach {container} --sig-proxy=false"),
|
||||
Some(format!("docker attach {container} --sig-proxy=false")),
|
||||
recreate,
|
||||
Some((container, ContainerTerminalMode::Attach)),
|
||||
)
|
||||
@@ -257,14 +258,15 @@ impl Resolve<super::Args> for DisconnectTerminal {
|
||||
fields(
|
||||
id = args.id.to_string(),
|
||||
core = args.core,
|
||||
channel_id = self.id.to_string(),
|
||||
channel_id = self.channel.to_string(),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &super::Args,
|
||||
) -> anyhow::Result<NoData> {
|
||||
if let Some(channel) = terminal_channels().remove(&self.id).await
|
||||
if let Some(channel) =
|
||||
terminal_channels().remove(&self.channel).await
|
||||
{
|
||||
channel.cancel.cancel();
|
||||
}
|
||||
@@ -363,7 +365,7 @@ impl Resolve<super::Args> for ExecuteContainerExec {
|
||||
|
||||
let terminal = create_terminal(
|
||||
container.clone(),
|
||||
format!("docker exec -it {container} {shell}"),
|
||||
Some(format!("docker exec -it {container} {shell}")),
|
||||
recreate,
|
||||
Some((container, ContainerTerminalMode::Exec)),
|
||||
)
|
||||
@@ -445,13 +447,13 @@ async fn handle_terminal_forwarding(
|
||||
let (a, b) = terminal.history.bytes_parts();
|
||||
if !a.is_empty() {
|
||||
sender
|
||||
.send_terminal(channel, a)
|
||||
.send_terminal(channel, Ok(a.into()))
|
||||
.await
|
||||
.context("Failed to send history part a")?;
|
||||
}
|
||||
if !b.is_empty() {
|
||||
sender
|
||||
.send_terminal(channel, b)
|
||||
.send_terminal(channel, Ok(b.into()))
|
||||
.await
|
||||
.context("Failed to send history part b")?;
|
||||
}
|
||||
@@ -472,32 +474,44 @@ async fn handle_terminal_forwarding(
|
||||
let res = tokio::select! {
|
||||
res = stdout.recv() => res,
|
||||
_ = terminal.cancel.cancelled() => {
|
||||
trace!("ws write: cancelled from outside");
|
||||
// let _ = ws_sender.send("PTY KILLED")).await;
|
||||
// if let Err(e) = ws_write.close().await {
|
||||
// debug!("Failed to close ws: {e:?}");
|
||||
// };
|
||||
let _ = sender.send_terminal(channel, Err(anyhow!(
|
||||
"\n{} {}",
|
||||
"pty".bold(),
|
||||
"exited".red().bold()
|
||||
))).await;
|
||||
break
|
||||
},
|
||||
_ = cancel.cancelled() => {
|
||||
// let _ = ws_write.send(Message::Text(Utf8Bytes::from_static("WS KILLED"))).await;
|
||||
// if let Err(e) = ws_write.close().await {
|
||||
// debug!("Failed to close ws: {e:?}");
|
||||
// };
|
||||
let _ = sender.send_terminal(channel, Err(anyhow!(
|
||||
"\n{} {}",
|
||||
"websocket".bold(),
|
||||
"disconnected".red().bold()
|
||||
))).await;
|
||||
break
|
||||
}
|
||||
};
|
||||
|
||||
let bytes = match res {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
debug!("Terminal receiver failed | {e:?}");
|
||||
Err(_e) => {
|
||||
terminal.cancel();
|
||||
let _ = sender
|
||||
.send_terminal(
|
||||
channel,
|
||||
Err(anyhow!(
|
||||
"\n{} {}",
|
||||
"pty".bold(),
|
||||
"exited".red().bold()
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = sender.send_terminal(channel, bytes).await {
|
||||
if let Err(e) =
|
||||
sender.send_terminal(channel, Ok(bytes.into())).await
|
||||
{
|
||||
debug!("Failed to send to WS: {e:?}");
|
||||
cancel.cancel();
|
||||
break;
|
||||
@@ -583,14 +597,17 @@ async fn forward_execute_command_on_terminal_response(
|
||||
loop {
|
||||
match stdout.next().await {
|
||||
Some(Ok(line)) if line.as_str() == END_OF_OUTPUT => {
|
||||
if let Err(e) = sender.send_terminal(channel, line).await {
|
||||
if let Err(e) =
|
||||
sender.send_terminal(channel, Ok(line.into())).await
|
||||
{
|
||||
warn!("Got ws_sender send error on END_OF_OUTPUT | {e:?}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
Some(Ok(line)) => {
|
||||
if let Err(e) =
|
||||
sender.send_terminal(channel, line + "\n").await
|
||||
if let Err(e) = sender
|
||||
.send_terminal(channel, Ok((line + "\n").into()))
|
||||
.await
|
||||
{
|
||||
warn!("Got ws_sender send error | {e:?}");
|
||||
break;
|
||||
|
||||
@@ -108,6 +108,9 @@ pub fn periphery_config() -> &'static PeripheryConfig {
|
||||
repo_dir: env.periphery_repo_dir.or(config.repo_dir),
|
||||
stack_dir: env.periphery_stack_dir.or(config.stack_dir),
|
||||
build_dir: env.periphery_build_dir.or(config.build_dir),
|
||||
default_terminal_command: env
|
||||
.periphery_default_terminal_command
|
||||
.unwrap_or(config.default_terminal_command),
|
||||
disable_terminals: env
|
||||
.periphery_disable_terminals
|
||||
.unwrap_or(config.disable_terminals),
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{collections::HashMap, sync::Arc};
|
||||
use anyhow::{Context, anyhow};
|
||||
use async_timing_util::wait_until_timelength;
|
||||
use bollard::{models, query_parameters::StatsOptionsBuilder};
|
||||
use futures::StreamExt;
|
||||
use futures_util::StreamExt;
|
||||
use komodo_client::entities::docker::{
|
||||
container::ContainerStats,
|
||||
stats::{
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{
|
||||
net::IpAddr, path::PathBuf, str::FromStr as _, sync::OnceLock,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use command::run_komodo_command_with_sanitization;
|
||||
@@ -204,6 +206,57 @@ pub fn registry_token(
|
||||
.with_context(|| format!("did not find token in config for docker registry account {account_username} | domain {domain}"))
|
||||
}
|
||||
|
||||
// ====================
|
||||
// Public IP over DNS
|
||||
// ====================
|
||||
|
||||
type OpenDNSResolver = hickory_resolver::Resolver<
|
||||
hickory_resolver::name_server::TokioConnectionProvider,
|
||||
>;
|
||||
|
||||
fn opendns_resolver() -> &'static OpenDNSResolver {
|
||||
static OPENDNS_RESOLVER: OnceLock<OpenDNSResolver> =
|
||||
OnceLock::new();
|
||||
OPENDNS_RESOLVER.get_or_init(|| {
|
||||
// OpenDNS resolver ips
|
||||
let ips = [
|
||||
IpAddr::from_str("208.67.222.222").unwrap(),
|
||||
IpAddr::from_str("208.67.220.220").unwrap(),
|
||||
IpAddr::from_str("2620:119:35::35").unwrap(),
|
||||
IpAddr::from_str("2620:119:53::53").unwrap(),
|
||||
];
|
||||
|
||||
// trust_negative_responses=true means NXDOMAIN/empty NOERROR from an
|
||||
// authoritative upstream won’t be retried on other servers.
|
||||
let ns =
|
||||
hickory_resolver::config::NameServerConfigGroup::from_ips_clear(
|
||||
&ips, 53, true,
|
||||
);
|
||||
|
||||
hickory_resolver::Resolver::builder_with_config(
|
||||
hickory_resolver::config::ResolverConfig::from_parts(
|
||||
None,
|
||||
vec![],
|
||||
ns,
|
||||
),
|
||||
hickory_resolver::name_server::TokioConnectionProvider::default(
|
||||
),
|
||||
)
|
||||
.build()
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn resolve_host_public_ip() -> anyhow::Result<String> {
|
||||
opendns_resolver()
|
||||
.lookup_ip("myip.opendns.com.")
|
||||
.await
|
||||
.context("Failed to query OpenDNS resolvers for host public IP")?
|
||||
.into_iter()
|
||||
.map(|ip| ip.to_string())
|
||||
.next()
|
||||
.context("OpenDNS call for public IP didn't return anything")
|
||||
}
|
||||
|
||||
// =====
|
||||
// SSL
|
||||
// =====
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use futures::{StreamExt, stream::FuturesUnordered};
|
||||
use futures_util::{StreamExt, stream::FuturesUnordered};
|
||||
use komodo_client::entities::config::periphery::Command;
|
||||
use tracing::Instrument;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use cache::CloneCache;
|
||||
use komodo_client::entities::docker::container::ContainerStats;
|
||||
use noise::key::{RotatableKeyPair, SpkiPublicKey};
|
||||
use periphery_client::transport::EncodedTransportMessage;
|
||||
use tokio::sync::{Mutex, RwLock, mpsc, oneshot};
|
||||
use tokio::sync::{Mutex, OnceCell, RwLock, mpsc, oneshot};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use transport::channel::BufferedChannel;
|
||||
use uuid::Uuid;
|
||||
@@ -18,6 +18,7 @@ use uuid::Uuid;
|
||||
use crate::{
|
||||
config::periphery_config,
|
||||
docker::DockerClient,
|
||||
helpers::resolve_host_public_ip,
|
||||
stats::StatsClient,
|
||||
terminal::{StdinMsg, Terminal},
|
||||
};
|
||||
@@ -297,3 +298,18 @@ pub fn container_stats() -> &'static ArcSwap<ContainerStatsMap> {
|
||||
OnceLock::new();
|
||||
CONTAINER_STATS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub async fn host_public_ip() -> Option<&'static String> {
|
||||
static PUBLIC_IPS: OnceCell<Option<String>> = OnceCell::const_new();
|
||||
PUBLIC_IPS
|
||||
.get_or_init(|| async {
|
||||
resolve_host_public_ip()
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
warn!("Failed to resolve host public ip | {e:#}")
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.await
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
@@ -5,19 +5,24 @@ use bytes::Bytes;
|
||||
use encoding::{Decode as _, WithChannel};
|
||||
use komodo_client::{
|
||||
api::write::TerminalRecreateMode,
|
||||
entities::{ContainerTerminalMode, server::TerminalInfo},
|
||||
entities::{
|
||||
ContainerTerminalMode, komodo_timestamp, server::TerminalInfo,
|
||||
},
|
||||
};
|
||||
use periphery_client::transport::EncodedTerminalMessage;
|
||||
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::state::{terminal_channels, terminal_triggers, terminals};
|
||||
use crate::{
|
||||
config::periphery_config,
|
||||
state::{terminal_channels, terminal_triggers, terminals},
|
||||
};
|
||||
|
||||
pub async fn handle_message(message: EncodedTerminalMessage) {
|
||||
let WithChannel {
|
||||
channel: channel_id,
|
||||
mut data,
|
||||
data,
|
||||
} = match message.decode() {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
@@ -25,6 +30,16 @@ pub async fn handle_message(message: EncodedTerminalMessage) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut data = match data {
|
||||
Ok(data) => data,
|
||||
Err(_) => {
|
||||
// This means Core should disconnect.
|
||||
terminal_channels().remove(&channel_id).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let msg = match data.first() {
|
||||
Some(&0x00) => StdinMsg::Bytes(data.drain(1..).collect()),
|
||||
Some(&0xFF) => {
|
||||
@@ -45,11 +60,13 @@ pub async fn handle_message(message: EncodedTerminalMessage) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(channel) = terminal_channels().get(&channel_id).await
|
||||
else {
|
||||
warn!("No terminal channel for {channel_id}");
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(e) = channel.sender.send(msg).await {
|
||||
warn!("No receiver for {channel_id} | {e:?}");
|
||||
};
|
||||
@@ -58,10 +75,13 @@ pub async fn handle_message(message: EncodedTerminalMessage) {
|
||||
#[instrument("CreateTerminalInner", skip_all, fields(name))]
|
||||
pub async fn create_terminal(
|
||||
name: String,
|
||||
command: String,
|
||||
command: Option<String>,
|
||||
recreate: TerminalRecreateMode,
|
||||
container: Option<(String, ContainerTerminalMode)>,
|
||||
) -> anyhow::Result<Arc<Terminal>> {
|
||||
let command = command.unwrap_or_else(|| {
|
||||
periphery_config().default_terminal_command.clone()
|
||||
});
|
||||
trace!(
|
||||
"CreateTerminal: {name} | command: {command} | recreate: {recreate:?}"
|
||||
);
|
||||
@@ -120,6 +140,7 @@ pub async fn list_terminals(
|
||||
name: name.to_string(),
|
||||
command: terminal.command.clone(),
|
||||
stored_size_kb: terminal.history.size_kb(),
|
||||
created_at: terminal.created_at,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
terminals.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
@@ -173,6 +194,8 @@ pub type StdoutReceiver = broadcast::Receiver<Bytes>;
|
||||
pub struct Terminal {
|
||||
/// The command that was used as the root command, eg `shell`
|
||||
command: String,
|
||||
/// Created timestamp milliseconds
|
||||
created_at: i64,
|
||||
|
||||
pub cancel: CancellationToken,
|
||||
|
||||
@@ -356,6 +379,7 @@ impl Terminal {
|
||||
stdout,
|
||||
history,
|
||||
container,
|
||||
created_at: komodo_timestamp(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ resolver_api.workspace = true
|
||||
# external
|
||||
tokio-tungstenite.workspace = true
|
||||
derive_builder.workspace = true
|
||||
futures-util.workspace = true
|
||||
urlencoding.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio-util.workspace = true
|
||||
@@ -36,7 +37,6 @@ thiserror.workspace = true
|
||||
typeshare.workspace = true
|
||||
indexmap.workspace = true
|
||||
serde_qs.workspace = true
|
||||
futures.workspace = true
|
||||
reqwest.workspace = true
|
||||
tracing.workspace = true
|
||||
anyhow.workspace = true
|
||||
@@ -46,5 +46,4 @@ strum.workspace = true
|
||||
bson.workspace = true
|
||||
clap.workspace = true
|
||||
envy.workspace = true
|
||||
uuid.workspace = true
|
||||
rand.workspace = true
|
||||
@@ -7,7 +7,7 @@ use crate::entities::{
|
||||
I64, Timelength,
|
||||
server::{
|
||||
PeripheryInformation, Server, ServerActionState, ServerListItem,
|
||||
ServerQuery, ServerState, TerminalInfo,
|
||||
ServerQuery, ServerState, TerminalInfo, TerminalInfoWithServer,
|
||||
},
|
||||
stats::{
|
||||
SystemInformation, SystemProcess, SystemStats, SystemStatsRecord,
|
||||
@@ -289,3 +289,27 @@ pub struct ListTerminals {
|
||||
|
||||
#[typeshare]
|
||||
pub type ListTerminalsResponse = Vec<TerminalInfo>;
|
||||
|
||||
//
|
||||
|
||||
/// List the current terminals on specified server.
|
||||
/// Response: [ListAllTerminalsResponse].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoReadRequest)]
|
||||
#[response(ListAllTerminalsResponse)]
|
||||
#[error(serror::Error)]
|
||||
pub struct ListAllTerminals {
|
||||
/// optional structured query to filter servers.
|
||||
#[serde(default)]
|
||||
pub query: ServerQuery,
|
||||
/// Force a fresh call to Periphery for the list.
|
||||
/// Otherwise the response will be cached for 30s
|
||||
#[serde(default)]
|
||||
pub fresh: bool,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type ListAllTerminalsResponse = Vec<TerminalInfoWithServer>;
|
||||
|
||||
@@ -26,6 +26,25 @@ pub struct ExecuteTerminalBody {
|
||||
pub terminal: String,
|
||||
/// The command to execute.
|
||||
pub command: String,
|
||||
/// Pass to init the terminal session
|
||||
/// for when the terminal doesn't already exist.
|
||||
pub init: Option<InitTerminal>,
|
||||
}
|
||||
|
||||
/// Init a terminal on the server.
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct InitTerminal {
|
||||
/// The shell command (eg `bash`) to init the shell.
|
||||
///
|
||||
/// This can also include args:
|
||||
/// `docker exec -it container sh`
|
||||
///
|
||||
/// Default: Configured on each Periphery
|
||||
pub command: Option<String>,
|
||||
/// Default: `Never`
|
||||
#[serde(default)]
|
||||
pub recreate: TerminalRecreateMode,
|
||||
}
|
||||
|
||||
/// Query to connect to a container exec session (interactive shell over websocket) on the given server.
|
||||
@@ -40,8 +59,7 @@ pub struct ConnectContainerExecQuery {
|
||||
pub shell: String,
|
||||
/// Specify the recreate behavior.
|
||||
/// Default is 'DifferentCommand'
|
||||
#[serde(default = "default_container_recreate_mode")]
|
||||
pub recreate: TerminalRecreateMode,
|
||||
pub recreate: Option<TerminalRecreateMode>,
|
||||
}
|
||||
|
||||
/// Query to connect to a container attach session (interactive shell over websocket) on the given server.
|
||||
@@ -54,8 +72,7 @@ pub struct ConnectContainerAttachQuery {
|
||||
pub container: String,
|
||||
/// Specify the recreate behavior.
|
||||
/// Default is 'DifferentCommand'
|
||||
#[serde(default = "default_container_recreate_mode")]
|
||||
pub recreate: TerminalRecreateMode,
|
||||
pub recreate: Option<TerminalRecreateMode>,
|
||||
}
|
||||
|
||||
/// Execute a command in the given containers shell.
|
||||
@@ -72,8 +89,7 @@ pub struct ExecuteContainerExecBody {
|
||||
pub command: String,
|
||||
/// Specify the recreate behavior.
|
||||
/// Default is 'DifferentCommand'
|
||||
#[serde(default = "default_container_recreate_mode")]
|
||||
pub recreate: TerminalRecreateMode,
|
||||
pub recreate: Option<TerminalRecreateMode>,
|
||||
}
|
||||
|
||||
/// Query to connect to a container exec session (interactive shell over websocket) on the given Deployment.
|
||||
@@ -87,8 +103,7 @@ pub struct ConnectDeploymentExecQuery {
|
||||
pub shell: String,
|
||||
/// Specify the recreate behavior.
|
||||
/// Default is 'DifferentCommand'
|
||||
#[serde(default = "default_container_recreate_mode")]
|
||||
pub recreate: TerminalRecreateMode,
|
||||
pub recreate: Option<TerminalRecreateMode>,
|
||||
}
|
||||
|
||||
/// Query to connect to a container attach session (interactive shell over websocket) on the given Deployment.
|
||||
@@ -100,8 +115,7 @@ pub struct ConnectDeploymentAttachQuery {
|
||||
pub deployment: String,
|
||||
/// Specify the recreate behavior.
|
||||
/// Default is 'DifferentCommand'
|
||||
#[serde(default = "default_container_recreate_mode")]
|
||||
pub recreate: TerminalRecreateMode,
|
||||
pub recreate: Option<TerminalRecreateMode>,
|
||||
}
|
||||
|
||||
/// Execute a command in the given containers shell.
|
||||
@@ -116,8 +130,7 @@ pub struct ExecuteDeploymentExecBody {
|
||||
pub command: String,
|
||||
/// Specify the recreate behavior.
|
||||
/// Default is 'DifferentCommand'
|
||||
#[serde(default = "default_container_recreate_mode")]
|
||||
pub recreate: TerminalRecreateMode,
|
||||
pub recreate: Option<TerminalRecreateMode>,
|
||||
}
|
||||
|
||||
/// Query to connect to a container exec session (interactive shell over websocket) on the given Stack / service.
|
||||
@@ -133,8 +146,7 @@ pub struct ConnectStackExecQuery {
|
||||
pub shell: String,
|
||||
/// Specify the recreate behavior.
|
||||
/// Default is 'DifferentCommand'
|
||||
#[serde(default = "default_container_recreate_mode")]
|
||||
pub recreate: TerminalRecreateMode,
|
||||
pub recreate: Option<TerminalRecreateMode>,
|
||||
}
|
||||
|
||||
/// Query to connect to a container attach session (interactive shell over websocket) on the given Stack / service.
|
||||
@@ -148,8 +160,7 @@ pub struct ConnectStackAttachQuery {
|
||||
pub service: String,
|
||||
/// Specify the recreate behavior.
|
||||
/// Default is 'DifferentCommand'
|
||||
#[serde(default = "default_container_recreate_mode")]
|
||||
pub recreate: TerminalRecreateMode,
|
||||
pub recreate: Option<TerminalRecreateMode>,
|
||||
}
|
||||
|
||||
/// Execute a command in the given containers shell.
|
||||
@@ -166,10 +177,5 @@ pub struct ExecuteStackExecBody {
|
||||
pub command: String,
|
||||
/// Specify the recreate behavior.
|
||||
/// Default is 'DifferentCommand'
|
||||
#[serde(default = "default_container_recreate_mode")]
|
||||
pub recreate: TerminalRecreateMode,
|
||||
}
|
||||
|
||||
fn default_container_recreate_mode() -> TerminalRecreateMode {
|
||||
TerminalRecreateMode::DifferentCommand
|
||||
pub recreate: Option<TerminalRecreateMode>,
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use typeshare::typeshare;
|
||||
|
||||
use crate::entities::{
|
||||
NoData,
|
||||
server::{_PartialServerConfig, Server},
|
||||
server::{_PartialServerConfig, Server, ServerQuery},
|
||||
update::Update,
|
||||
};
|
||||
|
||||
@@ -165,18 +165,13 @@ pub struct CreateTerminal {
|
||||
/// This can also include args:
|
||||
/// `docker exec -it container sh`
|
||||
///
|
||||
/// Default: `bash`
|
||||
#[serde(default = "default_command")]
|
||||
pub command: String,
|
||||
/// Default: Configured on each Periphery
|
||||
pub command: Option<String>,
|
||||
/// Default: `Never`
|
||||
#[serde(default)]
|
||||
pub recreate: TerminalRecreateMode,
|
||||
}
|
||||
|
||||
fn default_command() -> String {
|
||||
String::from("bash")
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/// Delete a terminal on the server.
|
||||
@@ -213,6 +208,23 @@ pub struct DeleteAllTerminals {
|
||||
|
||||
//
|
||||
|
||||
/// Delete all terminals on many or all Servers.
|
||||
/// Response: [NoData]
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(NoData)]
|
||||
#[error(serror::Error)]
|
||||
pub struct BatchDeleteAllTerminals {
|
||||
/// optional structured query to filter servers.
|
||||
#[serde(default)]
|
||||
pub query: ServerQuery,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/// Updates the Server with an explicit Public Key.
|
||||
/// Response: [Update]
|
||||
#[typeshare]
|
||||
|
||||
@@ -98,6 +98,9 @@ pub enum ListCommand {
|
||||
/// List Syncs (aliases: `sync`, `sn`)
|
||||
#[clap(alias = "sync", alias = "sn")]
|
||||
Syncs(ResourceFilters),
|
||||
/// List Terminals (aliases: `term`, `tm`)
|
||||
#[clap(alias = "term", alias = "tm")]
|
||||
Terminals(ResourceFilters),
|
||||
/// List scheduled Procedures / Actions (aliases: `sched`, `sc`)
|
||||
#[clap(alias = "sched", alias = "sc")]
|
||||
Schedules(ResourceFilters),
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::api::execute::Execution;
|
||||
pub mod container;
|
||||
pub mod database;
|
||||
pub mod list;
|
||||
pub mod ssh;
|
||||
pub mod update;
|
||||
|
||||
#[derive(Debug, clap::Parser)]
|
||||
@@ -51,20 +52,6 @@ pub enum Command {
|
||||
unsanitized: bool,
|
||||
},
|
||||
|
||||
/// Private-Public key utilities. (alias: `k`)
|
||||
#[clap(alias = "k")]
|
||||
Key {
|
||||
#[command(subcommand)]
|
||||
command: crate::entities::config::KeyCommand,
|
||||
},
|
||||
|
||||
/// Database utilities. (alias: `db`)
|
||||
#[clap(alias = "db")]
|
||||
Database {
|
||||
#[command(subcommand)]
|
||||
command: database::DatabaseCommand,
|
||||
},
|
||||
|
||||
/// Container info (aliases: `ps`, `cn`, `containers`)
|
||||
#[clap(alias = "ps", alias = "cn", alias = "containers")]
|
||||
Container(container::Container),
|
||||
@@ -93,6 +80,22 @@ pub enum Command {
|
||||
#[command(subcommand)]
|
||||
command: update::UpdateCommand,
|
||||
},
|
||||
|
||||
Ssh(ssh::Ssh),
|
||||
|
||||
/// Private-Public key utilities. (alias: `k`)
|
||||
#[clap(alias = "k")]
|
||||
Key {
|
||||
#[command(subcommand)]
|
||||
command: crate::entities::config::KeyCommand,
|
||||
},
|
||||
|
||||
/// Database utilities. (alias: `db`)
|
||||
#[clap(alias = "db")]
|
||||
Database {
|
||||
#[command(subcommand)]
|
||||
command: database::DatabaseCommand,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, clap::Parser)]
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#[derive(Debug, Clone, clap::Parser)]
|
||||
pub struct Ssh {
|
||||
/// The server to connect to.
|
||||
pub server: String,
|
||||
|
||||
/// Custom command to use to start the shell, eg `bash`.
|
||||
/// Defaults to Periphery default.
|
||||
pub command: Option<String>,
|
||||
|
||||
/// The terminal name to connect to. Default: `ssh`
|
||||
#[arg(long, short = 'n', default_value_t = String::from("ssh"))]
|
||||
pub name: String,
|
||||
|
||||
/// Force fresh terminal to replace existing one.
|
||||
#[arg(long, short = 'r', default_value_t = false)]
|
||||
pub recreate: bool,
|
||||
}
|
||||
@@ -173,6 +173,8 @@ pub struct Env {
|
||||
pub periphery_stack_dir: Option<PathBuf>,
|
||||
/// Override `build_dir`
|
||||
pub periphery_build_dir: Option<PathBuf>,
|
||||
/// Override `default_terminal_command`
|
||||
pub periphery_default_terminal_command: Option<String>,
|
||||
/// Override `disable_terminals`
|
||||
pub periphery_disable_terminals: Option<bool>,
|
||||
/// Override `disable_container_terminals`
|
||||
@@ -356,6 +358,12 @@ pub struct PeripheryConfig {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub build_dir: Option<PathBuf>,
|
||||
|
||||
/// Configure the default terminal command
|
||||
/// when one isn't provided.
|
||||
/// Default: `bash`
|
||||
#[serde(default = "default_default_terminal_command")]
|
||||
pub default_terminal_command: String,
|
||||
|
||||
/// Whether to disable the create terminal
|
||||
/// and disallow direct remote shell access.
|
||||
/// Default: false
|
||||
@@ -431,6 +439,10 @@ fn default_root_directory() -> PathBuf {
|
||||
"/etc/komodo".parse().unwrap()
|
||||
}
|
||||
|
||||
fn default_default_terminal_command() -> String {
|
||||
String::from("bash")
|
||||
}
|
||||
|
||||
fn default_stats_polling_rate() -> Timelength {
|
||||
Timelength::FiveSeconds
|
||||
}
|
||||
@@ -460,6 +472,7 @@ impl Default for PeripheryConfig {
|
||||
repo_dir: None,
|
||||
stack_dir: None,
|
||||
build_dir: None,
|
||||
default_terminal_command: default_default_terminal_command(),
|
||||
disable_terminals: Default::default(),
|
||||
disable_container_terminals: Default::default(),
|
||||
stats_polling_rate: default_stats_polling_rate(),
|
||||
@@ -510,6 +523,7 @@ impl PeripheryConfig {
|
||||
repo_dir: self.repo_dir.clone(),
|
||||
stack_dir: self.stack_dir.clone(),
|
||||
build_dir: self.build_dir.clone(),
|
||||
default_terminal_command: self.default_terminal_command.clone(),
|
||||
disable_terminals: self.disable_terminals,
|
||||
disable_container_terminals: self.disable_container_terminals,
|
||||
stats_polling_rate: self.stats_polling_rate,
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
deserializers::{
|
||||
option_string_list_deserializer, string_list_deserializer,
|
||||
},
|
||||
entities::{MaintenanceWindow, Timelength},
|
||||
entities::{I64, MaintenanceWindow, Timelength},
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -36,6 +36,8 @@ pub struct ServerListItemInfo {
|
||||
/// External address of the server (reachable by users).
|
||||
/// Used with links.
|
||||
pub external_address: Option<String>,
|
||||
/// Host public ip, if it could be resolved.
|
||||
pub public_ip: Option<String>,
|
||||
/// Whether server is configured to send disconnected alerts.
|
||||
pub send_unreachable_alerts: bool,
|
||||
/// Whether server is configured to send cpu alerts.
|
||||
@@ -358,6 +360,8 @@ pub struct PeripheryInformation {
|
||||
pub stats_polling_rate: Timelength,
|
||||
/// Whether Periphery is successfully connected to docker daemon.
|
||||
pub docker_connected: bool,
|
||||
/// The host public ip, if it can be resolved.
|
||||
pub public_ip: Option<String>,
|
||||
}
|
||||
|
||||
/// Info about an active terminal on a server.
|
||||
@@ -371,6 +375,49 @@ pub struct TerminalInfo {
|
||||
pub command: String,
|
||||
/// The size of the terminal history in memory.
|
||||
pub stored_size_kb: f64,
|
||||
/// When the Terminal was created.
|
||||
pub created_at: I64,
|
||||
}
|
||||
|
||||
/// Info about an active terminal on a server.
|
||||
/// Retrieve with [ListAllTerminals][crate::api::read::server::ListAllTerminals].
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
pub struct TerminalInfoWithServer {
|
||||
/// The server id.
|
||||
pub server_id: String,
|
||||
/// The server name.
|
||||
pub server_name: String,
|
||||
/// The name of the terminal.
|
||||
pub name: String,
|
||||
/// The root program / args of the pty
|
||||
pub command: String,
|
||||
/// The size of the terminal history in memory.
|
||||
pub stored_size_kb: f64,
|
||||
/// When the Terminal was created in unix milliseconds.
|
||||
pub created_at: I64,
|
||||
}
|
||||
|
||||
impl TerminalInfoWithServer {
|
||||
pub fn from_terminal_info(
|
||||
server_id: impl Into<String>,
|
||||
server_name: impl Into<String>,
|
||||
TerminalInfo {
|
||||
name,
|
||||
command,
|
||||
stored_size_kb,
|
||||
created_at,
|
||||
}: TerminalInfo,
|
||||
) -> Self {
|
||||
Self {
|
||||
server_id: server_id.into(),
|
||||
server_name: server_name.into(),
|
||||
name,
|
||||
command,
|
||||
stored_size_kb,
|
||||
created_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Current pending actions on the server.
|
||||
|
||||
@@ -192,12 +192,12 @@ impl Log {
|
||||
/// Combines stdout / stderr into one log
|
||||
pub fn combined(&self) -> String {
|
||||
match (self.stdout.is_empty(), self.stderr.is_empty()) {
|
||||
(true, true) => {
|
||||
(false, false) => {
|
||||
format!("stdout: {}\n\nstderr: {}", self.stdout, self.stderr)
|
||||
}
|
||||
(true, false) => self.stdout.to_string(),
|
||||
(false, true) => self.stderr.to_string(),
|
||||
(false, false) => String::from("No log"),
|
||||
(false, true) => self.stdout.to_string(),
|
||||
(true, false) => self.stderr.to_string(),
|
||||
(true, true) => String::from("No log"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use futures::{Stream, StreamExt, TryStreamExt};
|
||||
use futures_util::{Stream, StreamExt, TryStreamExt};
|
||||
|
||||
pub struct TerminalStreamResponse(pub reqwest::Response);
|
||||
|
||||
|
||||
@@ -1,288 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use futures::{SinkExt, TryStreamExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serror::serialize_error;
|
||||
use thiserror::Error;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{Instrument, debug, info, info_span, warn};
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{KomodoClient, entities::update::UpdateListItem};
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
pub enum WsLoginMessage {
|
||||
Jwt { jwt: String },
|
||||
ApiKeys { key: String, secret: String },
|
||||
}
|
||||
|
||||
impl WsLoginMessage {
|
||||
pub fn from_json_str(json: &str) -> anyhow::Result<WsLoginMessage> {
|
||||
serde_json::from_str(json)
|
||||
.context("failed to parse json as WsLoginMessage")
|
||||
}
|
||||
|
||||
pub fn to_json_string(&self) -> anyhow::Result<String> {
|
||||
serde_json::to_string(self)
|
||||
.context("failed to serialize WsLoginMessage to json string")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum UpdateWsMessage {
|
||||
Update(UpdateListItem),
|
||||
Error(UpdateWsError),
|
||||
Disconnected,
|
||||
Reconnected,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, Clone)]
|
||||
pub enum UpdateWsError {
|
||||
#[error("failed to connect | {0}")]
|
||||
ConnectionError(String),
|
||||
#[error("failed to login | {0}")]
|
||||
LoginError(String),
|
||||
#[error("failed to recieve message | {0}")]
|
||||
MessageError(String),
|
||||
#[error("did not recognize message | {0}")]
|
||||
MessageUnrecognized(String),
|
||||
}
|
||||
|
||||
const MAX_SHORT_RETRY_COUNT: usize = 5;
|
||||
|
||||
impl KomodoClient {
|
||||
/// Subscribes to the Komodo Core update websocket,
|
||||
/// and forwards the updates over a channel.
|
||||
/// Handles reconnection internally.
|
||||
///
|
||||
/// ```text
|
||||
/// let (mut rx, _) = komodo.subscribe_to_updates()?;
|
||||
/// loop {
|
||||
/// let update = match rx.recv().await {
|
||||
/// Ok(msg) => msg,
|
||||
/// Err(e) => {
|
||||
/// error!("🚨 recv error | {e:?}");
|
||||
/// break;
|
||||
/// }
|
||||
/// };
|
||||
/// // Handle the update
|
||||
/// info!("Got update: {update:?}");
|
||||
/// }
|
||||
/// ```
|
||||
pub fn subscribe_to_updates(
|
||||
&self,
|
||||
// retry_cooldown_secs: u64,
|
||||
) -> anyhow::Result<(
|
||||
broadcast::Receiver<UpdateWsMessage>,
|
||||
CancellationToken,
|
||||
)> {
|
||||
let (tx, rx) = broadcast::channel(128);
|
||||
let cancel = CancellationToken::new();
|
||||
let cancel_clone = cancel.clone();
|
||||
let address =
|
||||
format!("{}/ws/update", self.address.replacen("http", "ws", 1));
|
||||
let login_msg = WsLoginMessage::ApiKeys {
|
||||
key: self.key.clone(),
|
||||
secret: self.secret.clone(),
|
||||
}
|
||||
.to_json_string()?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
let master_uuid = Uuid::new_v4();
|
||||
loop {
|
||||
// OUTER LOOP (LONG RECONNECT)
|
||||
if cancel.is_cancelled() {
|
||||
break;
|
||||
}
|
||||
|
||||
let outer_uuid = Uuid::new_v4();
|
||||
let span = info_span!(
|
||||
"Outer Loop",
|
||||
master_uuid = format!("{master_uuid}"),
|
||||
outer_uuid = format!("{outer_uuid}")
|
||||
);
|
||||
|
||||
async {
|
||||
debug!("Entering inner (connection) loop | outer uuid {outer_uuid} | master uuid {master_uuid}");
|
||||
let mut retry = 0;
|
||||
loop {
|
||||
// INNER LOOP (SHORT RECONNECT)
|
||||
if cancel.is_cancelled() {
|
||||
break;
|
||||
}
|
||||
if retry >= MAX_SHORT_RETRY_COUNT {
|
||||
break;
|
||||
}
|
||||
|
||||
let inner_uuid = Uuid::new_v4();
|
||||
let span = info_span!(
|
||||
"Inner Loop",
|
||||
master_uuid = format!("{master_uuid}"),
|
||||
outer_uuid = format!("{outer_uuid}"),
|
||||
inner_uuid = format!("{inner_uuid}")
|
||||
);
|
||||
|
||||
async {
|
||||
debug!("Connecting to websocket | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}");
|
||||
|
||||
let mut ws =
|
||||
match connect_async(&address).await.with_context(|| {
|
||||
format!(
|
||||
"failed to connect to Komodo update websocket at {address}"
|
||||
)
|
||||
}) {
|
||||
Ok((ws, _)) => ws,
|
||||
Err(e) => {
|
||||
let _ = tx.send(UpdateWsMessage::Error(
|
||||
UpdateWsError::ConnectionError(serialize_error(&e)),
|
||||
));
|
||||
warn!("{e:#}");
|
||||
retry += 1;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
debug!("Connected to websocket | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}");
|
||||
|
||||
// ==================
|
||||
// SEND LOGIN MSG
|
||||
// ==================
|
||||
let login_send_res = ws
|
||||
.send(Message::text(&login_msg))
|
||||
.await
|
||||
.context("failed to send login message");
|
||||
|
||||
if let Err(e) = login_send_res {
|
||||
let _ = tx.send(UpdateWsMessage::Error(
|
||||
UpdateWsError::LoginError(serialize_error(&e)),
|
||||
));
|
||||
warn!("breaking inner loop | {e:#} | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}");
|
||||
retry += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// ==================
|
||||
// HANDLE LOGIN RES
|
||||
// ==================
|
||||
match ws.try_next().await {
|
||||
Ok(Some(Message::Text(msg))) => {
|
||||
if msg != "LOGGED_IN" {
|
||||
let _ = tx.send(UpdateWsMessage::Error(
|
||||
UpdateWsError::LoginError(msg.to_string()),
|
||||
));
|
||||
let _ = ws.close(None).await;
|
||||
warn!("breaking inner loop | got msg {msg} instead of 'LOGGED_IN' | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}");
|
||||
retry += 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
Ok(Some(msg)) => {
|
||||
let _ = tx.send(UpdateWsMessage::Error(
|
||||
UpdateWsError::LoginError(format!("{msg:#?}")),
|
||||
));
|
||||
let _ = ws.close(None).await;
|
||||
warn!("breaking inner loop | got msg {msg} instead of Message::Text 'LOGGED_IN' | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}");
|
||||
retry += 1;
|
||||
return;
|
||||
}
|
||||
Ok(None) => {
|
||||
let _ = tx.send(UpdateWsMessage::Error(
|
||||
UpdateWsError::LoginError(String::from(
|
||||
"got None message after login message",
|
||||
)),
|
||||
));
|
||||
let _ = ws.close(None).await;
|
||||
warn!("breaking inner loop | got None instead of 'LOGGED_IN' | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}");
|
||||
retry += 1;
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = tx.send(UpdateWsMessage::Error(
|
||||
UpdateWsError::LoginError(format!(
|
||||
"failed to recieve message | {e:#?}"
|
||||
)),
|
||||
));
|
||||
let _ = ws.close(None).await;
|
||||
warn!("breaking inner loop | got error msg | {e:?} | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}");
|
||||
retry += 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = tx.send(UpdateWsMessage::Reconnected);
|
||||
|
||||
info!("Logged into websocket | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}");
|
||||
|
||||
// If we get to this point (connected / logged in) reset the short retry counter
|
||||
retry = 0;
|
||||
|
||||
// ==================
|
||||
// HANLDE MSGS
|
||||
// ==================
|
||||
loop {
|
||||
match ws
|
||||
.try_next()
|
||||
.await
|
||||
.context("failed to recieve message")
|
||||
{
|
||||
Ok(Some(Message::Text(msg))) => {
|
||||
match serde_json::from_str::<UpdateListItem>(&msg) {
|
||||
Ok(msg) => {
|
||||
debug!(
|
||||
"got recognized message: {msg:?} | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}"
|
||||
);
|
||||
let _ = tx.send(UpdateWsMessage::Update(msg));
|
||||
}
|
||||
Err(_) => {
|
||||
warn!(
|
||||
"got unrecognized message: {msg:?} | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}"
|
||||
);
|
||||
let _ = tx.send(UpdateWsMessage::Error(
|
||||
UpdateWsError::MessageUnrecognized(msg.to_string()),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(Message::Close(_))) => {
|
||||
let _ = tx.send(UpdateWsMessage::Disconnected);
|
||||
let _ = ws.close(None).await;
|
||||
warn!(
|
||||
"breaking inner loop | got close message | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}"
|
||||
);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = tx.send(UpdateWsMessage::Error(
|
||||
UpdateWsError::MessageError(serialize_error(&e)),
|
||||
));
|
||||
let _ = tx.send(UpdateWsMessage::Disconnected);
|
||||
let _ = ws.close(None).await;
|
||||
warn!(
|
||||
"breaking inner loop | got error message | {e:#} | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}"
|
||||
);
|
||||
break;
|
||||
}
|
||||
Ok(_) => {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await;
|
||||
}
|
||||
}.instrument(span).await;
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
}
|
||||
});
|
||||
|
||||
Ok((rx, cancel_clone))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
use std::fmt::Write;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use futures_util::{SinkExt as _, TryStreamExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_tungstenite::{
|
||||
MaybeTlsStream, WebSocketStream, tungstenite,
|
||||
};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::KomodoClient;
|
||||
|
||||
pub mod update;
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
pub enum WsLoginMessage {
|
||||
Jwt { jwt: String },
|
||||
ApiKeys { key: String, secret: String },
|
||||
}
|
||||
|
||||
impl WsLoginMessage {
|
||||
pub fn from_json_str(json: &str) -> anyhow::Result<WsLoginMessage> {
|
||||
serde_json::from_str(json)
|
||||
.context("failed to parse json as WsLoginMessage")
|
||||
}
|
||||
|
||||
pub fn to_json_string(&self) -> anyhow::Result<String> {
|
||||
serde_json::to_string(self)
|
||||
.context("failed to serialize WsLoginMessage to json string")
|
||||
}
|
||||
}
|
||||
|
||||
impl KomodoClient {
|
||||
pub async fn connect_terminal_websocket(
|
||||
&self,
|
||||
server: &str,
|
||||
terminal: &str,
|
||||
) -> anyhow::Result<WebSocketStream<MaybeTlsStream<TcpStream>>> {
|
||||
self
|
||||
.connect_login_user_websocket(
|
||||
"/terminal",
|
||||
Some(&format!("server={server}&terminal={terminal}")),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn connect_login_user_websocket(
|
||||
&self,
|
||||
path: &str,
|
||||
query: Option<&str>,
|
||||
) -> anyhow::Result<WebSocketStream<MaybeTlsStream<TcpStream>>> {
|
||||
let mut endpoint =
|
||||
format!("{}/ws{path}", self.address.replacen("http", "ws", 1));
|
||||
if let Some(query) = query {
|
||||
write!(&mut endpoint, "?{query}")?;
|
||||
}
|
||||
let login_msg = WsLoginMessage::ApiKeys {
|
||||
key: self.key.clone(),
|
||||
secret: self.secret.clone(),
|
||||
}
|
||||
.to_json_string()?;
|
||||
let (mut ws, _) = tokio_tungstenite::connect_async(&endpoint)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("failed to connect to Komodo websocket at {endpoint}")
|
||||
})?;
|
||||
ws.send(tungstenite::Message::Text(login_msg.into()))
|
||||
.await
|
||||
.context("Failed to send websocket login message")?;
|
||||
loop {
|
||||
match ws
|
||||
.try_next()
|
||||
.await
|
||||
.context("Failed to receive websocket login response")?
|
||||
{
|
||||
Some(tungstenite::Message::Text(msg)) => {
|
||||
if msg == "LOGGED_IN" {
|
||||
return Ok(ws);
|
||||
} else {
|
||||
let _ = ws.close(None).await;
|
||||
return Err(anyhow!("Failed to log in | {msg}"));
|
||||
}
|
||||
}
|
||||
Some(tungstenite::Message::Close(_)) | None => {
|
||||
let _ = ws.close(None).await;
|
||||
return Err(anyhow!("Socket closed before login"));
|
||||
}
|
||||
// Keep looping on other message types
|
||||
Some(_) => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use futures_util::TryStreamExt;
|
||||
use serror::serialize_error;
|
||||
use thiserror::Error;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{KomodoClient, entities::update::UpdateListItem};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum UpdateWsMessage {
|
||||
Update(UpdateListItem),
|
||||
Error(UpdateWsError),
|
||||
Disconnected,
|
||||
Reconnected,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, Clone)]
|
||||
pub enum UpdateWsError {
|
||||
#[error("Failed to connect | {0}")]
|
||||
ConnectionError(String),
|
||||
#[error("Failed to recieve message | {0}")]
|
||||
MessageError(String),
|
||||
#[error("Did not recognize message | {0}")]
|
||||
MessageUnrecognized(String),
|
||||
}
|
||||
|
||||
const MAX_SHORT_RETRY_COUNT: usize = 5;
|
||||
|
||||
impl KomodoClient {
|
||||
/// Subscribes to the Komodo Core update websocket,
|
||||
/// and forwards the updates over a channel.
|
||||
/// Handles reconnection internally.
|
||||
///
|
||||
/// ```text
|
||||
/// let (mut rx, _) = komodo.subscribe_to_updates()?;
|
||||
/// loop {
|
||||
/// let update = match rx.recv().await {
|
||||
/// Ok(msg) => msg,
|
||||
/// Err(e) => {
|
||||
/// error!("🚨 recv error | {e:?}");
|
||||
/// break;
|
||||
/// }
|
||||
/// };
|
||||
/// // Handle the update
|
||||
/// info!("Got update: {update:?}");
|
||||
/// }
|
||||
/// ```
|
||||
pub fn subscribe_to_updates(
|
||||
self,
|
||||
// retry_cooldown_secs: u64,
|
||||
) -> anyhow::Result<(
|
||||
broadcast::Receiver<UpdateWsMessage>,
|
||||
CancellationToken,
|
||||
)> {
|
||||
let (tx, rx) = broadcast::channel(128);
|
||||
let cancel = CancellationToken::new();
|
||||
let cancel_clone = cancel.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
// OUTER LOOP (LONG RECONNECT)
|
||||
if cancel.is_cancelled() {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut retry = 0;
|
||||
loop {
|
||||
// INNER LOOP (SHORT RECONNECT)
|
||||
if cancel.is_cancelled() {
|
||||
break;
|
||||
}
|
||||
if retry >= MAX_SHORT_RETRY_COUNT {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut ws = match self
|
||||
.connect_login_user_websocket("/update", None)
|
||||
.await
|
||||
{
|
||||
Ok(ws) => ws,
|
||||
Err(e) => {
|
||||
let _ = tx.send(UpdateWsMessage::Error(
|
||||
UpdateWsError::ConnectionError(serialize_error(&e)),
|
||||
));
|
||||
retry += 1;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let _ = tx.send(UpdateWsMessage::Reconnected);
|
||||
|
||||
// If we get to this point (connected / logged in) reset the short retry counter
|
||||
retry = 0;
|
||||
|
||||
// ==================
|
||||
// HANLDE MSGS
|
||||
// ==================
|
||||
loop {
|
||||
match ws
|
||||
.try_next()
|
||||
.await
|
||||
.context("failed to recieve message")
|
||||
{
|
||||
Ok(Some(Message::Text(msg))) => {
|
||||
match serde_json::from_str::<UpdateListItem>(&msg) {
|
||||
Ok(msg) => {
|
||||
let _ = tx.send(UpdateWsMessage::Update(msg));
|
||||
}
|
||||
Err(_) => {
|
||||
let _ = tx.send(UpdateWsMessage::Error(
|
||||
UpdateWsError::MessageUnrecognized(
|
||||
msg.to_string(),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(Message::Close(_))) => {
|
||||
let _ = tx.send(UpdateWsMessage::Disconnected);
|
||||
let _ = ws.close(None).await;
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = tx.send(UpdateWsMessage::Error(
|
||||
UpdateWsError::MessageError(serialize_error(&e)),
|
||||
));
|
||||
let _ = tx.send(UpdateWsMessage::Disconnected);
|
||||
let _ = ws.close(None).await;
|
||||
break;
|
||||
}
|
||||
Ok(_) => {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
}
|
||||
});
|
||||
|
||||
Ok((rx, cancel_clone))
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,7 @@ export type ReadResponses = {
|
||||
ListServers: Types.ListServersResponse;
|
||||
ListFullServers: Types.ListFullServersResponse;
|
||||
ListTerminals: Types.ListTerminalsResponse;
|
||||
ListAllTerminals: Types.ListAllTerminalsResponse;
|
||||
|
||||
// ==== DOCKER ====
|
||||
GetDockerContainersSummary: Types.GetDockerContainersSummaryResponse;
|
||||
@@ -219,6 +220,7 @@ export type WriteResponses = {
|
||||
CreateTerminal: Types.NoData;
|
||||
DeleteTerminal: Types.NoData;
|
||||
DeleteAllTerminals: Types.NoData;
|
||||
BatchDeleteAllTerminals: Types.NoData;
|
||||
UpdateServerPublicKey: Types.Update;
|
||||
RotateServerKeys: Types.Update;
|
||||
|
||||
|
||||
+85
-11
@@ -1771,6 +1771,10 @@ export interface PeripheryInformation {
|
||||
container_terminals_disabled: boolean;
|
||||
/** The rate the system stats are being polled from the system */
|
||||
stats_polling_rate: Timelength;
|
||||
/** Whether Periphery is successfully connected to docker daemon. */
|
||||
docker_connected: boolean;
|
||||
/** The host public ip, if it can be resolved. */
|
||||
public_ip?: string;
|
||||
}
|
||||
|
||||
export type GetPeripheryInformationResponse = PeripheryInformation;
|
||||
@@ -3550,6 +3554,27 @@ export interface ContainerListItem {
|
||||
|
||||
export type ListAllDockerContainersResponse = ContainerListItem[];
|
||||
|
||||
/**
|
||||
* Info about an active terminal on a server.
|
||||
* Retrieve with [ListAllTerminals][crate::api::read::server::ListAllTerminals].
|
||||
*/
|
||||
export interface TerminalInfoWithServer {
|
||||
/** The server id. */
|
||||
server_id: string;
|
||||
/** The server name. */
|
||||
server_name: string;
|
||||
/** The name of the terminal. */
|
||||
name: string;
|
||||
/** The root program / args of the pty */
|
||||
command: string;
|
||||
/** The size of the terminal history in memory. */
|
||||
stored_size_kb: number;
|
||||
/** When the Terminal was created in unix milliseconds. */
|
||||
created_at: I64;
|
||||
}
|
||||
|
||||
export type ListAllTerminalsResponse = TerminalInfoWithServer[];
|
||||
|
||||
/** An api key used to authenticate requests via request headers. */
|
||||
export interface ApiKey {
|
||||
/** Unique key associated with secret */
|
||||
@@ -3927,6 +3952,8 @@ export interface ServerListItemInfo {
|
||||
* Used with links.
|
||||
*/
|
||||
external_address?: string;
|
||||
/** Host public ip, if it could be resolved. */
|
||||
public_ip?: string;
|
||||
/** Whether server is configured to send disconnected alerts. */
|
||||
send_unreachable_alerts: boolean;
|
||||
/** Whether server is configured to send cpu alerts. */
|
||||
@@ -4094,6 +4121,8 @@ export interface TerminalInfo {
|
||||
command: string;
|
||||
/** The size of the terminal history in memory. */
|
||||
stored_size_kb: number;
|
||||
/** When the Terminal was created. */
|
||||
created_at: I64;
|
||||
}
|
||||
|
||||
export type ListTerminalsResponse = TerminalInfo[];
|
||||
@@ -4349,6 +4378,15 @@ export interface BatchCloneRepo {
|
||||
pattern: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all terminals on many or all Servers.
|
||||
* Response: [NoData]
|
||||
*/
|
||||
export interface BatchDeleteAllTerminals {
|
||||
/** optional structured query to filter servers. */
|
||||
query?: ServerQuery;
|
||||
}
|
||||
|
||||
/** Deploys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResponse]. */
|
||||
export interface BatchDeploy {
|
||||
/**
|
||||
@@ -4636,7 +4674,7 @@ export interface ConnectContainerAttachQuery {
|
||||
* Specify the recreate behavior.
|
||||
* Default is 'DifferentCommand'
|
||||
*/
|
||||
recreate: TerminalRecreateMode;
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
|
||||
/** Query to connect to a container exec session (interactive shell over websocket) on the given server. */
|
||||
@@ -4651,7 +4689,7 @@ export interface ConnectContainerExecQuery {
|
||||
* Specify the recreate behavior.
|
||||
* Default is 'DifferentCommand'
|
||||
*/
|
||||
recreate: TerminalRecreateMode;
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4665,7 +4703,7 @@ export interface ConnectDeploymentAttachQuery {
|
||||
* Specify the recreate behavior.
|
||||
* Default is 'DifferentCommand'
|
||||
*/
|
||||
recreate: TerminalRecreateMode;
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4681,7 +4719,7 @@ export interface ConnectDeploymentExecQuery {
|
||||
* Specify the recreate behavior.
|
||||
* Default is 'DifferentCommand'
|
||||
*/
|
||||
recreate: TerminalRecreateMode;
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4697,7 +4735,7 @@ export interface ConnectStackAttachQuery {
|
||||
* Specify the recreate behavior.
|
||||
* Default is 'DifferentCommand'
|
||||
*/
|
||||
recreate: TerminalRecreateMode;
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4715,7 +4753,7 @@ export interface ConnectStackExecQuery {
|
||||
* Specify the recreate behavior.
|
||||
* Default is 'DifferentCommand'
|
||||
*/
|
||||
recreate: TerminalRecreateMode;
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
|
||||
/** Query to connect to a terminal (interactive shell over websocket) on the given server. */
|
||||
@@ -5306,9 +5344,9 @@ export interface CreateTerminal {
|
||||
* This can also include args:
|
||||
* `docker exec -it container sh`
|
||||
*
|
||||
* Default: `bash`
|
||||
* Default: Configured on each Periphery
|
||||
*/
|
||||
command: string;
|
||||
command?: string;
|
||||
/** Default: `Never` */
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
@@ -5697,7 +5735,7 @@ export interface ExecuteContainerExecBody {
|
||||
* Specify the recreate behavior.
|
||||
* Default is 'DifferentCommand'
|
||||
*/
|
||||
recreate: TerminalRecreateMode;
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
|
||||
/** Execute a command in the given containers shell. */
|
||||
@@ -5712,7 +5750,7 @@ export interface ExecuteDeploymentExecBody {
|
||||
* Specify the recreate behavior.
|
||||
* Default is 'DifferentCommand'
|
||||
*/
|
||||
recreate: TerminalRecreateMode;
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
|
||||
/** Execute a command in the given containers shell. */
|
||||
@@ -5729,7 +5767,22 @@ export interface ExecuteStackExecBody {
|
||||
* Specify the recreate behavior.
|
||||
* Default is 'DifferentCommand'
|
||||
*/
|
||||
recreate: TerminalRecreateMode;
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
|
||||
/** Init a terminal on the server. */
|
||||
export interface InitTerminal {
|
||||
/**
|
||||
* The shell command (eg `bash`) to init the shell.
|
||||
*
|
||||
* This can also include args:
|
||||
* `docker exec -it container sh`
|
||||
*
|
||||
* Default: Configured on each Periphery
|
||||
*/
|
||||
command?: string;
|
||||
/** Default: `Never` */
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
|
||||
/** Execute a terminal command on the given server. */
|
||||
@@ -5740,6 +5793,11 @@ export interface ExecuteTerminalBody {
|
||||
terminal: string;
|
||||
/** The command to execute. */
|
||||
command: string;
|
||||
/**
|
||||
* Pass to init the terminal session
|
||||
* for when the terminal doesn't already exist.
|
||||
*/
|
||||
init?: InitTerminal;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -6691,6 +6749,20 @@ export interface ListAllDockerContainers {
|
||||
servers?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* List the current terminals on specified server.
|
||||
* Response: [ListAllTerminalsResponse].
|
||||
*/
|
||||
export interface ListAllTerminals {
|
||||
/** optional structured query to filter servers. */
|
||||
query?: ServerQuery;
|
||||
/**
|
||||
* Force a fresh call to Periphery for the list.
|
||||
* Otherwise the response will be cached for 30s
|
||||
*/
|
||||
fresh?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets list of api keys for the calling user.
|
||||
* Response: [ListApiKeysResponse]
|
||||
@@ -8769,6 +8841,7 @@ export type ReadRequest =
|
||||
| { type: "ListServers", params: ListServers }
|
||||
| { type: "ListFullServers", params: ListFullServers }
|
||||
| { type: "ListTerminals", params: ListTerminals }
|
||||
| { type: "ListAllTerminals", params: ListAllTerminals }
|
||||
| { type: "GetDockerContainersSummary", params: GetDockerContainersSummary }
|
||||
| { type: "ListAllDockerContainers", params: ListAllDockerContainers }
|
||||
| { type: "ListDockerContainers", params: ListDockerContainers }
|
||||
@@ -8942,6 +9015,7 @@ export type WriteRequest =
|
||||
| { type: "CreateTerminal", params: CreateTerminal }
|
||||
| { type: "DeleteTerminal", params: DeleteTerminal }
|
||||
| { type: "DeleteAllTerminals", params: DeleteAllTerminals }
|
||||
| { type: "BatchDeleteAllTerminals", params: BatchDeleteAllTerminals }
|
||||
| { type: "UpdateServerPublicKey", params: UpdateServerPublicKey }
|
||||
| { type: "RotateServerKeys", params: RotateServerKeys }
|
||||
| { type: "CreateStack", params: CreateStack }
|
||||
|
||||
@@ -29,17 +29,14 @@ pub struct CreateTerminal {
|
||||
///
|
||||
/// This can also include args:
|
||||
/// `docker exec -it container sh`
|
||||
#[serde(default = "default_command")]
|
||||
pub command: String,
|
||||
///
|
||||
/// Default: Set in Periphery config.
|
||||
pub command: Option<String>,
|
||||
/// Default: `Never`
|
||||
#[serde(default)]
|
||||
pub recreate: TerminalRecreateMode,
|
||||
}
|
||||
|
||||
fn default_command() -> String {
|
||||
String::from("bash")
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]
|
||||
@@ -89,8 +86,8 @@ pub struct ConnectContainerAttach {
|
||||
#[response(NoData)]
|
||||
#[error(anyhow::Error)]
|
||||
pub struct DisconnectTerminal {
|
||||
/// The connection id of the terminal to disconnect from
|
||||
pub id: Uuid,
|
||||
/// The channel id of the terminal to disconnect from
|
||||
pub channel: Uuid,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -202,10 +202,12 @@ impl Decode<ResponseMessage> for EncodedResponseMessage {
|
||||
// ==================
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EncodedTerminalMessage(EncodedChannel<Vec<u8>>);
|
||||
pub struct EncodedTerminalMessage(
|
||||
EncodedChannel<EncodedResponse<Vec<u8>>>,
|
||||
);
|
||||
|
||||
impl TerminalMessage {
|
||||
pub fn new(channel: Uuid, bytes: Vec<u8>) -> Self {
|
||||
pub fn new(channel: Uuid, bytes: anyhow::Result<Vec<u8>>) -> Self {
|
||||
Self(WithChannel {
|
||||
channel,
|
||||
data: bytes,
|
||||
@@ -215,19 +217,27 @@ impl TerminalMessage {
|
||||
|
||||
impl_cast_bytes_vec!(EncodedTerminalMessage, EncodedChannel);
|
||||
|
||||
pub struct TerminalMessage(WithChannel<Vec<u8>>);
|
||||
pub struct TerminalMessage(WithChannel<anyhow::Result<Vec<u8>>>);
|
||||
|
||||
impl Encode<EncodedTransportMessage> for TerminalMessage {
|
||||
fn encode(self) -> EncodedTransportMessage {
|
||||
TransportMessage::Terminal(EncodedTerminalMessage(
|
||||
self.0.encode(),
|
||||
self.0.map_encode(),
|
||||
))
|
||||
.encode()
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<WithChannel<Vec<u8>>> for EncodedTerminalMessage {
|
||||
fn decode(self) -> anyhow::Result<WithChannel<Vec<u8>>> {
|
||||
self.0.decode_map()
|
||||
impl Decode<WithChannel<anyhow::Result<Vec<u8>>>>
|
||||
for EncodedTerminalMessage
|
||||
{
|
||||
fn decode(
|
||||
self,
|
||||
) -> anyhow::Result<WithChannel<anyhow::Result<Vec<u8>>>> {
|
||||
Ok(self.0.decode()?.map(|data| {
|
||||
data.decode().and_then(|r| {
|
||||
r.context("Terminal should not recieve Pending byte")
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,8 @@ services:
|
||||
# - /path/to/syncs:/syncs
|
||||
## Optionally mount a custom core.config.toml
|
||||
# - /path/to/core.config.toml:/config/config.toml
|
||||
## Optionally mount custom root CA certificate to trust
|
||||
# - /path/to/root_ca.crt:/usr/local/share/ca-certificates/root_ca.crt
|
||||
|
||||
## Deploy Periphery container using this block,
|
||||
## or deploy the Periphery binary with systemd using
|
||||
@@ -84,6 +86,8 @@ services:
|
||||
- ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}:${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}
|
||||
## Optionally mount a custom periphery.config.toml
|
||||
# - /path/to/periphery.config.toml:/config/config.toml
|
||||
## Optionally mount custom root CA certificate to trust
|
||||
# - /path/to/root_ca.crt:/usr/local/share/ca-certificates/root_ca.crt
|
||||
|
||||
volumes:
|
||||
# Postgres
|
||||
|
||||
@@ -42,6 +42,8 @@ services:
|
||||
# - /path/to/syncs:/syncs
|
||||
## Optionally mount a custom core.config.toml
|
||||
# - /path/to/core.config.toml:/config/config.toml
|
||||
## Optionally mount custom root CA certificate to trust
|
||||
# - /path/to/root_ca.crt:/usr/local/share/ca-certificates/root_ca.crt
|
||||
|
||||
## Deploy Periphery container using this block,
|
||||
## or deploy the Periphery binary with systemd using
|
||||
@@ -67,6 +69,8 @@ services:
|
||||
- ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}:${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}
|
||||
## Optionally mount a custom periphery.config.toml
|
||||
# - /path/to/periphery.config.toml:/config/config.toml
|
||||
## Optionally mount custom root CA certificate to trust
|
||||
# - /path/to/root_ca.crt:/usr/local/share/ca-certificates/root_ca.crt
|
||||
|
||||
volumes:
|
||||
# Mongo
|
||||
|
||||
@@ -54,6 +54,8 @@ services:
|
||||
- ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}:${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}
|
||||
## Optionally mount a custom periphery.config.toml
|
||||
# - /path/to/periphery.config.toml:/config/config.toml
|
||||
## Optionally mount custom root CA certificate to trust
|
||||
# - /path/to/root_ca.crt:/usr/local/share/ca-certificates/root_ca.crt
|
||||
|
||||
volumes:
|
||||
keys:
|
||||
@@ -24,9 +24,16 @@
|
||||
# GENERAL #
|
||||
###########
|
||||
|
||||
## The directory periphery will use as the default base for the directories it uses.
|
||||
## The periphery user must have write access to this directory.
|
||||
## Each specific directory (like stack_dir) can be overridden below.
|
||||
## Specify the root directory used by Periphery agent.
|
||||
## All your compose files and repos need to be inside this directory
|
||||
## for Periphery to interact with them.
|
||||
## - ROOT_DIRECTORY (/etc/komodo)
|
||||
## --- ./stacks
|
||||
## ------ ./my_stack_1
|
||||
## ------ ./my_stack_2
|
||||
## --- ./repos
|
||||
## ------ ./my_repo_1
|
||||
## Each specific sub-directory (like ./stacks) can be overridden below.
|
||||
## Env: PERIPHERY_ROOT_DIRECTORY
|
||||
## Default: /etc/komodo
|
||||
root_directory = "/etc/komodo"
|
||||
@@ -49,6 +56,12 @@ root_directory = "/etc/komodo"
|
||||
## Default: ${root_directory}/builds
|
||||
# build_dir = "/etc/komodo/builds"
|
||||
|
||||
## Set the default terminal command used to init the shell.
|
||||
## For example, `bash` or `zsh`.
|
||||
## Env: PERIPHERY_DEFAULT_TERMINAL_COMMAND
|
||||
## Default: bash
|
||||
default_terminal_command = "bash"
|
||||
|
||||
## Disable the terminal APIs and disallow remote shell access through Periphery.
|
||||
## Env: PERIPHERY_DISABLE_TERMINALS
|
||||
## Default: false
|
||||
|
||||
@@ -101,7 +101,7 @@ set at the top level of the file.
|
||||
#### Example File
|
||||
|
||||
The configuration can also be passed as **YAML** or **JSON**.
|
||||
You can use the it-tools to convert this TOML file to your preferred format:
|
||||
You can use it-tools to convert this TOML file to your preferred format:
|
||||
- YAML: https://it-tools.tech/toml-to-yaml
|
||||
- JSON: https://it-tools.tech/toml-to-json
|
||||
|
||||
|
||||
+17
-16
@@ -8,13 +8,13 @@ Komodo is a web app to provide structure for managing your servers, builds, depl
|
||||
|
||||
With Komodo you can:
|
||||
|
||||
- Connect all of your servers, alert on CPU usage, memory usage, and disk usage, and connect to shell sessions.
|
||||
- Create, start, stop, and restart Docker containers on the connected servers, view their status and logs, and connect to container shell.
|
||||
- Deploy docker compose stacks. The file can be defined in UI, or in a git repo, with auto deploy on git push.
|
||||
- Build application source into auto-versioned Docker images, auto built on webhook. Deploy single-use AWS instances for infinite capacity.
|
||||
- Manage repositories on connected servers, which can perform automation via scripting / webhooks.
|
||||
- Manage all your configuration / environment variables, with shared global variable and secret interpolation.
|
||||
- Keep a record of all the actions that are performed and by whom.
|
||||
- **Connect all of your servers**, alert on CPU usage, memory usage, and disk usage, and connect to shell sessions.
|
||||
- **Create, start, stop, and restart Docker containers** on the connected servers, view their status and logs, and connect to container shell.
|
||||
- **Deploy docker compose stacks.** The file can be defined in UI, or in a git repo, with auto deploy on git push.
|
||||
- **Build application source into auto-versioned Docker images**, auto built on webhook. Deploy single-use AWS instances for infinite capacity.
|
||||
- **Manage repositories on connected servers**, which can perform automation via scripting / webhooks.
|
||||
- **Manage all your configuration / environment variables**, with shared global variable and secret interpolation.
|
||||
- **Keep a record of all the changes** that are made and by whom.
|
||||
|
||||
There is no limit to the number of servers you can connect, and there will never be.
|
||||
There is no limit to what API you can use for automation, and there never will be.
|
||||
@@ -24,27 +24,28 @@ There is no limit to what API you can use for automation, and there never will b
|
||||
Komodo is opinionated by design, and uses [docker](https://docs.docker.com/) as the container engine for building and deploying.
|
||||
|
||||
:::info
|
||||
Komodo also supports [**podman**](https://podman.io/) instead of docker by utilizing the `podman` -> `docker` alias.
|
||||
For Stack / docker compose support with podman, check out [**podman-compose**](https://github.com/containers/podman-compose). Thanks to `u/pup_kit` for checking this.
|
||||
Komodo also can support [**podman**](https://podman.io/) instead of docker by utilizing the `podman` -> `docker` alias.
|
||||
:::
|
||||
|
||||
## Architecture and Components
|
||||
|
||||
Komodo is composed of a single Core and any amount of connected servers running the Periphery agent.
|
||||
Komodo is composed of a single Core and any amount of connected servers running the Periphery agent.
|
||||
|
||||
### Core
|
||||
Komodo Core is a web server hosting the Core API and browser UI. All user interaction with the connected servers flow through the Core.
|
||||
|
||||
Komodo Core is a **web server hosting the Core API and browser UI**. All user interaction with the connected servers flow through the Core.
|
||||
|
||||
### Periphery
|
||||
Komodo Periphery is a small stateless agent that runs on all connected servers. It exposes an API called by Komodo Core to perform actions on the server, get system usage, and container status / logs.
|
||||
In order to communicate with Core, it can be configured to both initiate outbound connections or accept inbound ones, whichever is simplest for your environment.
|
||||
|
||||
Komodo Periphery is a **small stateless agent** that runs on all connected servers. It exposes an API called by Komodo Core to perform actions on the server, get system usage, and container status / logs.
|
||||
In order to communicate with Core, it can be configured to **either initiate outbound connections or accept inbound ones**, whichever is simplest for your environment.
|
||||
|
||||
## Core API
|
||||
|
||||
Komodo exposes powerful functionality over the Core's REST and Websocket API, enabling infrastructure engineers to manage their infrastructure programmatically. There is a [rust crate](https://crates.io/crates/komodo_client) and [npm package](https://www.npmjs.com/package/komodo_client) to simplify programmatic interaction with the API, but in general this can be accomplished using any programming language that can make REST requests.
|
||||
Komodo exposes powerful functionality over **Core's REST and Websocket API**, enabling engineers to **implement complex automations easily** and manage their infrastructure programmatically. There is the [**Komodo CLI**](./ecosystem/cli.mdx), [**Rust crate**](https://crates.io/crates/komodo_client), and [**Npm package**](https://www.npmjs.com/package/komodo_client) to simplify programmatic interaction with the API, but this can also be accomplished just [using `curl`](https://docs.rs/komodo_client/latest/komodo_client/api/index.html#curl-example).
|
||||
|
||||
## Permissioning
|
||||
|
||||
Komodo is a system designed to be used by many users, whether they are developers, operations personnel, or administrators. The ability to affect an applications state is very powerful, so Komodo has a granular permissioning system to only provide this functionality to the intended users. The permissioning system is explained in detail in the [permissioning](/docs/resources/permissioning) section.
|
||||
Komodo is a system **designed to be used by many users working together**, whether they are developers, operations personnel, or administrators. The ability to affect an applications state is very powerful, so **Komodo has a granular permissioning system** to only provide this functionality to the intended users. The permissioning system is explained in more detail in the [permissioning](/docs/resources/permissioning) section.
|
||||
|
||||
User sign-on is possible using username / password, or with Oauth (Github, Google, and generic OIDC). See [Core Setup](./setup/index.mdx).
|
||||
User sign-on is possible using **username / password**, or with **Oauth (Github, Google, and generic OIDC)**. See [Core Setup](./setup/index.mdx).
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
# Advanced Configuration
|
||||
|
||||
### Custom Certificate Authorities
|
||||
|
||||
In order to communicate with companion services on private networks,
|
||||
such as OIDC and git providers,
|
||||
**Komodo Core and Periphery may both need to trust one or more custom CAs**.
|
||||
|
||||
**Starting in V2**, Both the Komodo Core and Periphery images will automatically `update-ca-certificates` on startup,
|
||||
just mount any required root certificates inside `/usr/local/share/ca-certificates`:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
## ... (unchanged)
|
||||
## Mount custom root CA certificates to trust individually
|
||||
- /path/to/root_ca1.crt:/usr/local/share/ca-certificates/root_ca1.crt
|
||||
- /path/to/root_ca2.crt:/usr/local/share/ca-certificates/root_ca2.crt
|
||||
## OR the whole folder is fine too.
|
||||
- /path/to/custom-certs:/usr/local/share/ca-certificates
|
||||
```
|
||||
|
||||
### OIDC / Oauth2
|
||||
|
||||
To enable OAuth2 login, you must create a client on the respective OAuth provider,
|
||||
@@ -37,7 +56,7 @@ If you prefer to keep sensitive information out of environment variables, you ca
|
||||
write a config file on your host, and mount it to `/config/config.toml` in the Komodo core container.
|
||||
|
||||
The configuration can also be passed as **YAML** or **JSON**.
|
||||
You can use the it-tools to convert this TOML file to your preferred format:
|
||||
You can use it-tools to convert this TOML file to your preferred format:
|
||||
- YAML: https://it-tools.tech/toml-to-yaml
|
||||
- JSON: https://it-tools.tech/toml-to-json
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ in `core.config.toml`, `komodo.cli.toml`, or in the Core container environment.
|
||||
```
|
||||
|
||||
:::warning
|
||||
Currently no encryption is supported,
|
||||
Currently no built-in encryption is supported,
|
||||
so you may want to encrypt the files before backing up remotely if your backup solution doesn't support that natively.
|
||||
:::
|
||||
|
||||
|
||||
@@ -6,25 +6,33 @@ import RemoteCodeFile from "@site/src/components/RemoteCodeFile";
|
||||
|
||||
Connecting a server to Komodo has 3 steps:
|
||||
|
||||
1. Add the Server to Komodo via the Core API / UI.
|
||||
2. Install and configure the Periphery agent on the server.
|
||||
1. **Create an Onboarding Key** via the Komodo Core API / UI (they can also be reused).
|
||||
2. **Install the Periphery agent** on the server or VM.
|
||||
3. Confirm the connection status is OK.
|
||||
|
||||
## Install Periphery
|
||||
|
||||
You can install Periphery as a systemd managed process, run it as a [docker container](https://github.com/moghtech/komodo/pkgs/container/komodo-periphery), or do whatever you want with the binary.
|
||||
You can install Periphery as a systemd managed process, run it as a [docker container](https://github.com/moghtech/komodo/pkgs/container/komodo-periphery), or run the binary directly with another process manager.
|
||||
|
||||
### Install the Periphery agent - systemd
|
||||
|
||||
As root user:
|
||||
**As root user:**
|
||||
```bash
|
||||
curl -sSL https://raw.githubusercontent.com/moghtech/komodo/main/scripts/setup-periphery.py | python3
|
||||
curl -sSL https://raw.githubusercontent.com/moghtech/komodo/main/scripts/setup-periphery.py \
|
||||
| python3 - \
|
||||
--core-address="https://<core-address>" \
|
||||
--connect-as="$(hostname)" \
|
||||
--onboarding-key="O-..."
|
||||
```
|
||||
|
||||
Periphery can also be installed to run as the calling user, just note this comes with some additional configuration.
|
||||
Periphery can also be installed to run as a **systemd user service** (for the calling user).
|
||||
|
||||
```bash
|
||||
curl -sSL https://raw.githubusercontent.com/moghtech/komodo/main/scripts/setup-periphery.py | python3 - --user
|
||||
curl -sSL https://raw.githubusercontent.com/moghtech/komodo/main/scripts/setup-periphery.py \
|
||||
| python3 - --user \
|
||||
--core-address="https://<core-address>" \
|
||||
--connect-as="$(hostname)" \
|
||||
--onboarding-key="O-..."
|
||||
```
|
||||
|
||||
You can find more information (and view the script) in the [readme](https://github.com/moghtech/komodo/tree/main/scripts).
|
||||
@@ -61,7 +69,10 @@ See the [periphery config docs](https://docs.rs/komodo_client/latest/komodo_clie
|
||||
for more information on configuring periphery.
|
||||
:::
|
||||
|
||||
3. Ensure that inbound connectivity is allowed on the port specified in periphery.config.toml (default 8120).
|
||||
3. Ensure that connectivity with Komodo Core is possible via the chosen connection method. **So either**:
|
||||
|
||||
- Periphery can reach Komodo Core at the configured `core_address`.
|
||||
- Komodo Core can reach Periphery on the specified port (default `8120`).
|
||||
|
||||
4. Install docker. See the [docker install docs](https://docs.docker.com/engine/install/).
|
||||
|
||||
@@ -91,7 +102,7 @@ command: periphery --config-path /path/in/container/to/periphery.config.base.tom
|
||||
## Configuration
|
||||
|
||||
The configuration can also be passed as **YAML** or **JSON**.
|
||||
You can use the it-tools to convert this TOML file to your preferred format:
|
||||
You can use it-tools to convert this TOML file to your preferred format:
|
||||
- YAML: https://it-tools.tech/toml-to-yaml
|
||||
- JSON: https://it-tools.tech/toml-to-json
|
||||
|
||||
|
||||
@@ -2,4 +2,7 @@
|
||||
|
||||
Most version upgrades only require a redeployment of the Core container after pulling the latest version, and are fully backward compatible with the periphery clients, which may be updated later on as convenient. This is the default, and will be the case unless specifically mentioned in the [version release notes](https://github.com/moghtech/komodo/releases).
|
||||
|
||||
Some Core API upgrades may change behavior such as building / cloning, and require updating the Periphery binaries to match the Core version before this functionality can be restored. This will be specifically mentioned in the release notes.
|
||||
Some Core API upgrades may change behavior such as building / cloning, and require updating the Periphery binaries to match the Core version before this functionality can be restored. This will be specifically mentioned in the release notes.
|
||||
|
||||
## Updating to Komodo v2
|
||||
|
||||
|
||||
+2
@@ -48,6 +48,7 @@ export type ReadResponses = {
|
||||
ListServers: Types.ListServersResponse;
|
||||
ListFullServers: Types.ListFullServersResponse;
|
||||
ListTerminals: Types.ListTerminalsResponse;
|
||||
ListAllTerminals: Types.ListAllTerminalsResponse;
|
||||
GetDockerContainersSummary: Types.GetDockerContainersSummaryResponse;
|
||||
ListAllDockerContainers: Types.ListAllDockerContainersResponse;
|
||||
ListDockerContainers: Types.ListDockerContainersResponse;
|
||||
@@ -160,6 +161,7 @@ export type WriteResponses = {
|
||||
CreateTerminal: Types.NoData;
|
||||
DeleteTerminal: Types.NoData;
|
||||
DeleteAllTerminals: Types.NoData;
|
||||
BatchDeleteAllTerminals: Types.NoData;
|
||||
UpdateServerPublicKey: Types.Update;
|
||||
RotateServerKeys: Types.Update;
|
||||
CreateStack: Types.Stack;
|
||||
|
||||
Vendored
+84
-11
@@ -1929,6 +1929,10 @@ export interface PeripheryInformation {
|
||||
container_terminals_disabled: boolean;
|
||||
/** The rate the system stats are being polled from the system */
|
||||
stats_polling_rate: Timelength;
|
||||
/** Whether Periphery is successfully connected to docker daemon. */
|
||||
docker_connected: boolean;
|
||||
/** The host public ip, if it can be resolved. */
|
||||
public_ip?: string;
|
||||
}
|
||||
export type GetPeripheryInformationResponse = PeripheryInformation;
|
||||
export type GetPermissionResponse = PermissionLevelAndSpecifics;
|
||||
@@ -3589,6 +3593,25 @@ export interface ContainerListItem {
|
||||
labels?: Record<string, string>;
|
||||
}
|
||||
export type ListAllDockerContainersResponse = ContainerListItem[];
|
||||
/**
|
||||
* Info about an active terminal on a server.
|
||||
* Retrieve with [ListAllTerminals][crate::api::read::server::ListAllTerminals].
|
||||
*/
|
||||
export interface TerminalInfoWithServer {
|
||||
/** The server id. */
|
||||
server_id: string;
|
||||
/** The server name. */
|
||||
server_name: string;
|
||||
/** The name of the terminal. */
|
||||
name: string;
|
||||
/** The root program / args of the pty */
|
||||
command: string;
|
||||
/** The size of the terminal history in memory. */
|
||||
stored_size_kb: number;
|
||||
/** When the Terminal was created in unix milliseconds. */
|
||||
created_at: I64;
|
||||
}
|
||||
export type ListAllTerminalsResponse = TerminalInfoWithServer[];
|
||||
/** An api key used to authenticate requests via request headers. */
|
||||
export interface ApiKey {
|
||||
/** Unique key associated with secret */
|
||||
@@ -3912,6 +3935,8 @@ export interface ServerListItemInfo {
|
||||
* Used with links.
|
||||
*/
|
||||
external_address?: string;
|
||||
/** Host public ip, if it could be resolved. */
|
||||
public_ip?: string;
|
||||
/** Whether server is configured to send disconnected alerts. */
|
||||
send_unreachable_alerts: boolean;
|
||||
/** Whether server is configured to send cpu alerts. */
|
||||
@@ -4066,6 +4091,8 @@ export interface TerminalInfo {
|
||||
command: string;
|
||||
/** The size of the terminal history in memory. */
|
||||
stored_size_kb: number;
|
||||
/** When the Terminal was created. */
|
||||
created_at: I64;
|
||||
}
|
||||
export type ListTerminalsResponse = TerminalInfo[];
|
||||
export type ListUserGroupsResponse = UserGroup[];
|
||||
@@ -4259,6 +4286,14 @@ export interface BatchCloneRepo {
|
||||
*/
|
||||
pattern: string;
|
||||
}
|
||||
/**
|
||||
* Delete all terminals on many or all Servers.
|
||||
* Response: [NoData]
|
||||
*/
|
||||
export interface BatchDeleteAllTerminals {
|
||||
/** optional structured query to filter servers. */
|
||||
query?: ServerQuery;
|
||||
}
|
||||
/** Deploys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResponse]. */
|
||||
export interface BatchDeploy {
|
||||
/**
|
||||
@@ -4526,7 +4561,7 @@ export interface ConnectContainerAttachQuery {
|
||||
* Specify the recreate behavior.
|
||||
* Default is 'DifferentCommand'
|
||||
*/
|
||||
recreate: TerminalRecreateMode;
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
/** Query to connect to a container exec session (interactive shell over websocket) on the given server. */
|
||||
export interface ConnectContainerExecQuery {
|
||||
@@ -4540,7 +4575,7 @@ export interface ConnectContainerExecQuery {
|
||||
* Specify the recreate behavior.
|
||||
* Default is 'DifferentCommand'
|
||||
*/
|
||||
recreate: TerminalRecreateMode;
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
/**
|
||||
* Query to connect to a container attach session (interactive shell over websocket) on the given Deployment.
|
||||
@@ -4553,7 +4588,7 @@ export interface ConnectDeploymentAttachQuery {
|
||||
* Specify the recreate behavior.
|
||||
* Default is 'DifferentCommand'
|
||||
*/
|
||||
recreate: TerminalRecreateMode;
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
/**
|
||||
* Query to connect to a container exec session (interactive shell over websocket) on the given Deployment.
|
||||
@@ -4568,7 +4603,7 @@ export interface ConnectDeploymentExecQuery {
|
||||
* Specify the recreate behavior.
|
||||
* Default is 'DifferentCommand'
|
||||
*/
|
||||
recreate: TerminalRecreateMode;
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
/**
|
||||
* Query to connect to a container attach session (interactive shell over websocket) on the given Stack / service.
|
||||
@@ -4583,7 +4618,7 @@ export interface ConnectStackAttachQuery {
|
||||
* Specify the recreate behavior.
|
||||
* Default is 'DifferentCommand'
|
||||
*/
|
||||
recreate: TerminalRecreateMode;
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
/**
|
||||
* Query to connect to a container exec session (interactive shell over websocket) on the given Stack / service.
|
||||
@@ -4600,7 +4635,7 @@ export interface ConnectStackExecQuery {
|
||||
* Specify the recreate behavior.
|
||||
* Default is 'DifferentCommand'
|
||||
*/
|
||||
recreate: TerminalRecreateMode;
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
/** Query to connect to a terminal (interactive shell over websocket) on the given server. */
|
||||
export interface ConnectTerminalQuery {
|
||||
@@ -5153,9 +5188,9 @@ export interface CreateTerminal {
|
||||
* This can also include args:
|
||||
* `docker exec -it container sh`
|
||||
*
|
||||
* Default: `bash`
|
||||
* Default: Configured on each Periphery
|
||||
*/
|
||||
command: string;
|
||||
command?: string;
|
||||
/** Default: `Never` */
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
@@ -5507,7 +5542,7 @@ export interface ExecuteContainerExecBody {
|
||||
* Specify the recreate behavior.
|
||||
* Default is 'DifferentCommand'
|
||||
*/
|
||||
recreate: TerminalRecreateMode;
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
/** Execute a command in the given containers shell. */
|
||||
export interface ExecuteDeploymentExecBody {
|
||||
@@ -5521,7 +5556,7 @@ export interface ExecuteDeploymentExecBody {
|
||||
* Specify the recreate behavior.
|
||||
* Default is 'DifferentCommand'
|
||||
*/
|
||||
recreate: TerminalRecreateMode;
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
/** Execute a command in the given containers shell. */
|
||||
export interface ExecuteStackExecBody {
|
||||
@@ -5537,7 +5572,21 @@ export interface ExecuteStackExecBody {
|
||||
* Specify the recreate behavior.
|
||||
* Default is 'DifferentCommand'
|
||||
*/
|
||||
recreate: TerminalRecreateMode;
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
/** Init a terminal on the server. */
|
||||
export interface InitTerminal {
|
||||
/**
|
||||
* The shell command (eg `bash`) to init the shell.
|
||||
*
|
||||
* This can also include args:
|
||||
* `docker exec -it container sh`
|
||||
*
|
||||
* Default: Configured on each Periphery
|
||||
*/
|
||||
command?: string;
|
||||
/** Default: `Never` */
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
/** Execute a terminal command on the given server. */
|
||||
export interface ExecuteTerminalBody {
|
||||
@@ -5547,6 +5596,11 @@ export interface ExecuteTerminalBody {
|
||||
terminal: string;
|
||||
/** The command to execute. */
|
||||
command: string;
|
||||
/**
|
||||
* Pass to init the terminal session
|
||||
* for when the terminal doesn't already exist.
|
||||
*/
|
||||
init?: InitTerminal;
|
||||
}
|
||||
/**
|
||||
* Get pretty formatted monrun sync toml for all resources
|
||||
@@ -6405,6 +6459,19 @@ export interface ListAllDockerContainers {
|
||||
/** Filter by server id or name. */
|
||||
servers?: string[];
|
||||
}
|
||||
/**
|
||||
* List the current terminals on specified server.
|
||||
* Response: [ListAllTerminalsResponse].
|
||||
*/
|
||||
export interface ListAllTerminals {
|
||||
/** optional structured query to filter servers. */
|
||||
query?: ServerQuery;
|
||||
/**
|
||||
* Force a fresh call to Periphery for the list.
|
||||
* Otherwise the response will be cached for 30s
|
||||
*/
|
||||
fresh?: boolean;
|
||||
}
|
||||
/**
|
||||
* Gets list of api keys for the calling user.
|
||||
* Response: [ListApiKeysResponse]
|
||||
@@ -8526,6 +8593,9 @@ export type ReadRequest = {
|
||||
} | {
|
||||
type: "ListTerminals";
|
||||
params: ListTerminals;
|
||||
} | {
|
||||
type: "ListAllTerminals";
|
||||
params: ListAllTerminals;
|
||||
} | {
|
||||
type: "GetDockerContainersSummary";
|
||||
params: GetDockerContainersSummary;
|
||||
@@ -8921,6 +8991,9 @@ export type WriteRequest = {
|
||||
} | {
|
||||
type: "DeleteAllTerminals";
|
||||
params: DeleteAllTerminals;
|
||||
} | {
|
||||
type: "BatchDeleteAllTerminals";
|
||||
params: BatchDeleteAllTerminals;
|
||||
} | {
|
||||
type: "UpdateServerPublicKey";
|
||||
params: UpdateServerPublicKey;
|
||||
|
||||
@@ -58,7 +58,7 @@ export const AlertDetailsDialogContent = ({
|
||||
return (
|
||||
<DialogContent className="w-[90vw] max-w-[900px]">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<ResourceLink
|
||||
type={alert.target.type as UsableResource}
|
||||
id={alert.target.id}
|
||||
@@ -71,7 +71,7 @@ export const AlertDetailsDialogContent = ({
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="flex gap-4 items-center flex-wrap">
|
||||
{/** Alert type */}
|
||||
<div className="flex gap-2">
|
||||
<div className="text-muted-foreground">type:</div>{" "}
|
||||
|
||||
@@ -23,10 +23,10 @@ import { useState } from "react";
|
||||
import {
|
||||
PlusCircle,
|
||||
Pen,
|
||||
Trash2,
|
||||
Clock,
|
||||
Calendar,
|
||||
CalendarDays,
|
||||
Trash,
|
||||
} from "lucide-react";
|
||||
import { TimezoneSelector } from "@components/util";
|
||||
|
||||
@@ -189,7 +189,7 @@ export const MaintenanceWindows = ({
|
||||
onClick={() => deleteWindow(row.index)}
|
||||
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<Trash className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -264,7 +264,7 @@ export const InputList = <T extends { [key: string]: unknown }>({
|
||||
set({ [field]: [...values] } as Partial<T>);
|
||||
}}
|
||||
disabled={disabled}
|
||||
className={cn("w-[400px] max-w-full", className)}
|
||||
className={cn("w-[230px] md:w-[400px]", className)}
|
||||
/>
|
||||
{!disabled && (
|
||||
<Button
|
||||
|
||||
@@ -32,6 +32,8 @@ export const Layout = () => {
|
||||
useShiftKeyListener("B", () => nav("/builds"));
|
||||
useShiftKeyListener("R", () => nav("/repos"));
|
||||
useShiftKeyListener("P", () => nav("/procedures"));
|
||||
useShiftKeyListener("X", () => nav("/terminals"));
|
||||
useShiftKeyListener("C", () => nav("/schedules"));
|
||||
useShiftKeyListener("V", () => {
|
||||
setSettingsView("Variables");
|
||||
nav("/settings");
|
||||
@@ -41,7 +43,7 @@ export const Layout = () => {
|
||||
<>
|
||||
<Topbar />
|
||||
<div className="h-screen overflow-y-scroll">
|
||||
<div className="container">
|
||||
<div className="container px-[1.2rem]">
|
||||
<Sidebar />
|
||||
<div className="lg:ml-[200px] lg:pl-8 py-[88px]">
|
||||
<Outlet />
|
||||
@@ -61,6 +63,7 @@ interface PageProps {
|
||||
subtitle?: ReactNode;
|
||||
actions?: ReactNode;
|
||||
superHeader?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Page = ({
|
||||
@@ -72,33 +75,15 @@ export const Page = ({
|
||||
subtitle,
|
||||
actions,
|
||||
children,
|
||||
}: PageProps) => (
|
||||
<div className="w-full flex flex-col gap-12">
|
||||
{superHeader ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
{superHeader}
|
||||
{(title || icon || subtitle || actions) && (
|
||||
<div
|
||||
className={`flex flex-col gap-6 md:flex-row md:gap-0 md:justify-between`}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap gap-4 items-center">
|
||||
{icon}
|
||||
<h1 className="text-4xl">{title}</h1>
|
||||
{titleRight}
|
||||
</div>
|
||||
<div className="flex flex-col">{subtitle}</div>
|
||||
</div>
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
(title || icon || subtitle || actions) && (
|
||||
className,
|
||||
}: PageProps) => {
|
||||
const Header = (
|
||||
<>
|
||||
{(title || icon || subtitle || actions) && (
|
||||
<div
|
||||
className={`flex flex-col gap-6 md:flex-row md:gap-0 md:justify-between`}
|
||||
>
|
||||
<div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap gap-4 items-center">
|
||||
{icon}
|
||||
<h1 className="text-4xl">{title}</h1>
|
||||
@@ -108,12 +93,24 @@ export const Page = ({
|
||||
</div>
|
||||
{actions}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{titleOther}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<div className={cn("w-full flex flex-col gap-12", className)}>
|
||||
{superHeader ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
{superHeader}
|
||||
{Header}
|
||||
</div>
|
||||
) : (
|
||||
Header
|
||||
)}
|
||||
{titleOther}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const PageXlRow = ({
|
||||
superHeader,
|
||||
@@ -193,7 +190,7 @@ export const Section = ({
|
||||
{(title || icon || titleRight || titleOther || actions) && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-wrap gap-2 justify-between",
|
||||
"flex flex-wrap gap-4 justify-between",
|
||||
itemsCenterTitleRow ? "items-center" : "items-start"
|
||||
)}
|
||||
>
|
||||
@@ -220,6 +217,8 @@ export const NewLayout = ({
|
||||
onConfirm,
|
||||
onOpenChange,
|
||||
configureLabel = "a unique name",
|
||||
open: _open,
|
||||
setOpen: _setOpen,
|
||||
}: {
|
||||
entityType: string;
|
||||
children: ReactNode;
|
||||
@@ -227,15 +226,19 @@ export const NewLayout = ({
|
||||
onConfirm: () => Promise<unknown>;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
configureLabel?: string;
|
||||
open?: boolean;
|
||||
setOpen?: (open: boolean) => void;
|
||||
}) => {
|
||||
const [open, set] = useState(false);
|
||||
const [__open, __setOpen] = useState(false);
|
||||
const open = _open ? _open : __open;
|
||||
const setOpen = _setOpen ? _setOpen : __setOpen;
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
set(open);
|
||||
setOpen(open);
|
||||
onOpenChange && onOpenChange(open);
|
||||
}}
|
||||
>
|
||||
@@ -251,7 +254,9 @@ export const NewLayout = ({
|
||||
Enter {configureLabel} for the new {entityType}.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-6 py-8">{children}</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -259,11 +264,11 @@ export const NewLayout = ({
|
||||
setLoading(true);
|
||||
try {
|
||||
await onConfirm();
|
||||
set(false);
|
||||
setOpen(false);
|
||||
} catch (error: any) {
|
||||
const status = error?.status || error?.response?.status;
|
||||
if (status !== 409 && status !== 400) {
|
||||
set(false);
|
||||
setOpen(false);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { useAllResources, useLocalStorage, useRead, useSettingsView, useUser } from "@lib/hooks";
|
||||
import {
|
||||
useAllResources,
|
||||
useLocalStorage,
|
||||
useRead,
|
||||
useSettingsView,
|
||||
useUser,
|
||||
} from "@lib/hooks";
|
||||
import { Button } from "@ui/button";
|
||||
import {
|
||||
CommandDialog,
|
||||
@@ -9,7 +15,7 @@ import {
|
||||
CommandSeparator,
|
||||
CommandItem,
|
||||
} from "@ui/command";
|
||||
import { Box, Home, Search, User } from "lucide-react";
|
||||
import { Box, CalendarDays, Home, Search, Terminal, User } from "lucide-react";
|
||||
import { Fragment, ReactNode, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { cn, RESOURCE_TARGETS, usableResourcePath } from "@lib/utils";
|
||||
@@ -162,6 +168,22 @@ const useOmniItems = (
|
||||
onSelect: () => nav("/containers"),
|
||||
template: false,
|
||||
},
|
||||
{
|
||||
key: "Terminals",
|
||||
type: "Server" as UsableResource,
|
||||
label: "Terminals",
|
||||
icon: <Terminal className="w-4 h-4" />,
|
||||
onSelect: () => nav("/terminals"),
|
||||
template: false,
|
||||
},
|
||||
{
|
||||
key: "Schedules",
|
||||
type: "Server" as UsableResource,
|
||||
label: "Schedules",
|
||||
icon: <CalendarDays className="w-4 h-4" />,
|
||||
onSelect: () => nav("/schedules"),
|
||||
template: false,
|
||||
},
|
||||
(user?.admin && {
|
||||
key: "Users",
|
||||
type: "Server" as UsableResource,
|
||||
|
||||
@@ -296,7 +296,7 @@ export const ActionConfig = ({ id }: { id: string }) => {
|
||||
<div className="text-nowrap text-muted-foreground text-sm">
|
||||
Listen on branch:
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<Input
|
||||
placeholder="Branch"
|
||||
value={branch}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "@lib/color";
|
||||
import { cn, updateLogToHtml } from "@lib/utils";
|
||||
import { Types } from "komodo_client";
|
||||
import { DashboardPieChart } from "@pages/home/dashboard";
|
||||
import { DashboardPieChart } from "@pages/dashboard";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/tooltip";
|
||||
import { Card } from "@ui/card";
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import { Section } from "@components/layouts";
|
||||
import {
|
||||
useInvalidate,
|
||||
useLocalStorage,
|
||||
useRead,
|
||||
useUser,
|
||||
useWrite,
|
||||
} from "@lib/hooks";
|
||||
import { useInvalidate, useRead, useUser, useWrite } from "@lib/hooks";
|
||||
import { RequiredResourceComponents } from "@types";
|
||||
import { Factory, FolderGit, Hammer, Loader2, RefreshCcw } from "lucide-react";
|
||||
import { BuildConfig } from "./config";
|
||||
import { BuildTable } from "./table";
|
||||
import {
|
||||
DeleteResource,
|
||||
@@ -17,7 +9,6 @@ import {
|
||||
ResourcePageHeader,
|
||||
StandardSource,
|
||||
} from "../common";
|
||||
import { DeploymentTable } from "../deployment/table";
|
||||
import { RunBuild } from "./actions";
|
||||
import {
|
||||
border_color_class_by_intention,
|
||||
@@ -25,10 +16,8 @@ import {
|
||||
stroke_color_class_by_intention,
|
||||
} from "@lib/color";
|
||||
import { cn } from "@lib/utils";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs";
|
||||
import { ResourceComponents } from "..";
|
||||
import { Types } from "komodo_client";
|
||||
import { DashboardPieChart } from "@pages/home/dashboard";
|
||||
import { DashboardPieChart } from "@pages/dashboard";
|
||||
import { StatusBadge } from "@components/util";
|
||||
import { Card } from "@ui/card";
|
||||
import { Badge } from "@ui/badge";
|
||||
@@ -37,7 +26,7 @@ import { Button } from "@ui/button";
|
||||
import { useBuilder } from "../builder";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/tooltip";
|
||||
import { BuildInfo } from "./info";
|
||||
import { BuildTabs } from "./tabs";
|
||||
|
||||
export const useBuild = (id?: string) =>
|
||||
useRead("ListBuilds", {}, { refetchInterval: 10_000 }).data?.find(
|
||||
@@ -53,55 +42,6 @@ const BuildIcon = ({ id, size }: { id?: string; size: number }) => {
|
||||
return <Hammer className={cn(`w-${size} h-${size}`, state && color)} />;
|
||||
};
|
||||
|
||||
const ConfigInfoDeployments = ({ id }: { id: string }) => {
|
||||
const [view, setView] = useLocalStorage<"Config" | "Info" | "Deployments">(
|
||||
"build-tabs-v1",
|
||||
"Config"
|
||||
);
|
||||
const deployments = useRead("ListDeployments", {}).data?.filter(
|
||||
(deployment) => deployment.info.build_id === id
|
||||
);
|
||||
const deploymentsDisabled = (deployments?.length || 0) === 0;
|
||||
const titleOther = (
|
||||
<TabsList className="justify-start w-fit">
|
||||
<TabsTrigger value="Config" className="w-[110px]">
|
||||
Config
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="Info" className="w-[110px]">
|
||||
Info
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="Deployments"
|
||||
className="w-[110px]"
|
||||
disabled={deploymentsDisabled}
|
||||
>
|
||||
Deployments
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
);
|
||||
return (
|
||||
<Tabs
|
||||
value={deploymentsDisabled && view === "Deployments" ? "Config" : view}
|
||||
onValueChange={setView as any}
|
||||
>
|
||||
<TabsContent value="Config">
|
||||
<BuildConfig id={id} titleOther={titleOther} />
|
||||
</TabsContent>
|
||||
<TabsContent value="Info">
|
||||
<BuildInfo id={id} titleOther={titleOther} />
|
||||
</TabsContent>
|
||||
<TabsContent value="Deployments">
|
||||
<Section
|
||||
titleOther={titleOther}
|
||||
actions={<ResourceComponents.Deployment.New build_id={id} />}
|
||||
>
|
||||
<DeploymentTable deployments={deployments ?? []} />
|
||||
</Section>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
export const BuildComponents: RequiredResourceComponents = {
|
||||
list_item: (id) => useBuild(id),
|
||||
resource_links: (resource) => (resource.config as Types.BuildConfig).links,
|
||||
@@ -139,12 +79,15 @@ export const BuildComponents: RequiredResourceComponents = {
|
||||
const builders = useRead("ListBuilders", {}).data;
|
||||
if (!user) return null;
|
||||
if (!user.admin && !user.create_build_permissions) return null;
|
||||
const builder_id =
|
||||
builders && builders.length === 1 ? builders[0].id : undefined;
|
||||
return (
|
||||
<NewResource
|
||||
type="Build"
|
||||
builder_id={
|
||||
builders && builders.length === 1 ? builders[0].id : undefined
|
||||
}
|
||||
selectBuilder={!builder_id}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -279,7 +222,7 @@ export const BuildComponents: RequiredResourceComponents = {
|
||||
|
||||
Page: {},
|
||||
|
||||
Config: ConfigInfoDeployments,
|
||||
Config: BuildTabs,
|
||||
|
||||
DangerZone: ({ id }) => <DeleteResource type="Build" id={id} />,
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useLocalStorage, useRead } from "@lib/hooks";
|
||||
import {
|
||||
MobileFriendlyTabsSelector,
|
||||
TabNoContent,
|
||||
} from "@ui/mobile-friendly-tabs";
|
||||
import { useMemo } from "react";
|
||||
import { BuildInfo } from "./info";
|
||||
import { Section } from "@components/layouts";
|
||||
import { ResourceComponents } from "..";
|
||||
import { DeploymentTable } from "../deployment/table";
|
||||
import { BuildConfig } from "./config";
|
||||
|
||||
type BuildTabsView = "Config" | "Info" | "Deployments";
|
||||
|
||||
export const BuildTabs = ({ id }: { id: string }) => {
|
||||
const [view, setView] = useLocalStorage<BuildTabsView>(
|
||||
"build-tabs-v1",
|
||||
"Config"
|
||||
);
|
||||
const deployments = useRead("ListDeployments", {}).data?.filter(
|
||||
(deployment) => deployment.info.build_id === id
|
||||
);
|
||||
const deploymentsDisabled = (deployments?.length || 0) === 0;
|
||||
|
||||
const tabsNoContent = useMemo<TabNoContent<BuildTabsView>[]>(
|
||||
() => [
|
||||
{
|
||||
value: "Config",
|
||||
},
|
||||
{
|
||||
value: "Info",
|
||||
},
|
||||
{
|
||||
value: "Deployments",
|
||||
disabled: deploymentsDisabled,
|
||||
},
|
||||
],
|
||||
[deploymentsDisabled]
|
||||
);
|
||||
|
||||
const Selector = (
|
||||
<MobileFriendlyTabsSelector
|
||||
tabs={tabsNoContent}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabsTriggerClassname="w-[110px]"
|
||||
/>
|
||||
);
|
||||
|
||||
switch (view) {
|
||||
case "Config":
|
||||
return <BuildConfig id={id} titleOther={Selector} />;
|
||||
case "Info":
|
||||
return <BuildInfo id={id} titleOther={Selector} />;
|
||||
case "Deployments":
|
||||
return (
|
||||
<Section
|
||||
titleOther={Selector}
|
||||
actions={<ResourceComponents.Deployment.New build_id={id} />}
|
||||
>
|
||||
<DeploymentTable deployments={deployments ?? []} />
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
useInvalidate,
|
||||
usePermissions,
|
||||
useRead,
|
||||
useShiftKeyListener,
|
||||
useWrite,
|
||||
WebhookIntegration,
|
||||
} from "@lib/hooks";
|
||||
@@ -296,6 +297,7 @@ export const ResourceSelector = ({
|
||||
align,
|
||||
templates = Types.TemplatesQueryBehavior.Exclude,
|
||||
placeholder,
|
||||
targetClassName,
|
||||
}: {
|
||||
type: UsableResource;
|
||||
selected: string | undefined;
|
||||
@@ -304,6 +306,7 @@ export const ResourceSelector = ({
|
||||
disabled?: boolean;
|
||||
align?: "start" | "center" | "end";
|
||||
placeholder?: string;
|
||||
targetClassName?: string;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
@@ -338,7 +341,10 @@ export const ResourceSelector = ({
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="flex justify-start gap-2 w-fit max-w-[350px]"
|
||||
className={cn(
|
||||
"flex justify-start gap-2 w-fit max-w-[350px]",
|
||||
targetClassName
|
||||
)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{name || (placeholder ?? `Select ${type}`)}
|
||||
@@ -505,6 +511,8 @@ export const NewResource = ({
|
||||
builder_id,
|
||||
build_id,
|
||||
name: _name = "",
|
||||
selectServer,
|
||||
selectBuilder,
|
||||
}: {
|
||||
type: UsableResource;
|
||||
readable_type?: string;
|
||||
@@ -512,6 +520,8 @@ export const NewResource = ({
|
||||
builder_id?: string;
|
||||
build_id?: string;
|
||||
name?: string;
|
||||
selectServer?: boolean;
|
||||
selectBuilder?: boolean;
|
||||
}) => {
|
||||
const nav = useNavigate();
|
||||
const { toast } = useToast();
|
||||
@@ -520,24 +530,29 @@ export const NewResource = ({
|
||||
0;
|
||||
const { mutateAsync: create } = useWrite(`Create${type}`);
|
||||
const { mutateAsync: copy } = useWrite(`Copy${type}`);
|
||||
const [templateId, setTemplateId] = useState<string>("");
|
||||
const [serverId, setServerId] = useState("");
|
||||
const [builderId, setBuilderId] = useState("");
|
||||
const [templateId, setTemplateId] = useState("");
|
||||
const [name, setName] = useState(_name);
|
||||
const type_display =
|
||||
type === "ResourceSync" ? "resource-sync" : type.toLowerCase();
|
||||
const config: Types._PartialDeploymentConfig | Types._PartialRepoConfig =
|
||||
type === "Deployment"
|
||||
? {
|
||||
server_id,
|
||||
server_id: server_id ?? serverId,
|
||||
image: build_id
|
||||
? { type: "Build", params: { build_id } }
|
||||
: { type: "Image", params: { image: "" } },
|
||||
}
|
||||
: type === "Stack"
|
||||
? { server_id }
|
||||
? { server_id: server_id ?? serverId }
|
||||
: type === "Repo"
|
||||
? { server_id, builder_id }
|
||||
? {
|
||||
server_id: server_id ?? serverId,
|
||||
builder_id: builder_id ?? builderId,
|
||||
}
|
||||
: type === "Build"
|
||||
? { builder_id }
|
||||
? { builder_id: builder_id ?? builderId }
|
||||
: {};
|
||||
const onConfirm = async () => {
|
||||
if (!name) toast({ title: "Name cannot be empty" });
|
||||
@@ -549,14 +564,18 @@ export const NewResource = ({
|
||||
nav(`/${usableResourcePath(type)}/${resourceId}`);
|
||||
}
|
||||
};
|
||||
const [open, setOpen] = useState(false);
|
||||
useShiftKeyListener("N", () => !open && setOpen(true));
|
||||
return (
|
||||
<NewLayout
|
||||
entityType={readable_type ?? type}
|
||||
onConfirm={onConfirm}
|
||||
enabled={!!name}
|
||||
onOpenChange={() => setName(_name)}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
>
|
||||
<div className="grid md:grid-cols-2 items-center">
|
||||
<div className="grid md:grid-cols-2 gap-6 items-center">
|
||||
{readable_type ?? type} Name
|
||||
<Input
|
||||
placeholder={`${type_display}-name`}
|
||||
@@ -569,20 +588,45 @@ export const NewResource = ({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{selectServer && (
|
||||
<>
|
||||
Server
|
||||
<ResourceSelector
|
||||
type="Server"
|
||||
selected={serverId}
|
||||
onSelect={setServerId}
|
||||
targetClassName="w-full justify-between"
|
||||
align="end"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{selectBuilder && (
|
||||
<>
|
||||
Builder
|
||||
<ResourceSelector
|
||||
type="Builder"
|
||||
selected={builderId}
|
||||
onSelect={setBuilderId}
|
||||
targetClassName="w-full justify-between"
|
||||
align="end"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{showTemplateSelector && (
|
||||
<>
|
||||
Template
|
||||
<ResourceSelector
|
||||
type={type}
|
||||
selected={templateId}
|
||||
onSelect={setTemplateId}
|
||||
templates={Types.TemplatesQueryBehavior.Only}
|
||||
placeholder="Template (Optional)"
|
||||
targetClassName="w-full justify-between"
|
||||
align="end"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{showTemplateSelector && (
|
||||
<div className="flex gap-4 justify-between items-center flex-wrap">
|
||||
Template
|
||||
<ResourceSelector
|
||||
type={type}
|
||||
selected={templateId}
|
||||
onSelect={setTemplateId}
|
||||
templates={Types.TemplatesQueryBehavior.Only}
|
||||
placeholder="Select Template"
|
||||
align="end"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</NewLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useLocalStorage, useRead } from "@lib/hooks";
|
||||
import { ConnectExecQuery, Types } from "komodo_client";
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { Types } from "komodo_client";
|
||||
import { RequiredResourceComponents } from "@types";
|
||||
import { CircleArrowUp, HardDrive, Rocket, Server } from "lucide-react";
|
||||
import { cn } from "@lib/utils";
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
PauseUnpauseDeployment,
|
||||
PullDeployment,
|
||||
} from "./actions";
|
||||
import { DeploymentLogs } from "./log";
|
||||
import {
|
||||
deployment_state_intention,
|
||||
stroke_color_class_by_intention,
|
||||
@@ -25,9 +24,7 @@ import {
|
||||
ResourcePageHeader,
|
||||
} from "../common";
|
||||
import { RunBuild } from "../build/actions";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs";
|
||||
import { DeploymentConfig } from "./config";
|
||||
import { DashboardPieChart } from "@pages/home/dashboard";
|
||||
import { DashboardPieChart } from "@pages/dashboard";
|
||||
import {
|
||||
ContainerPortsTableView,
|
||||
DockerResourceLink,
|
||||
@@ -35,10 +32,7 @@ import {
|
||||
} from "@components/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/tooltip";
|
||||
import { usePermissions } from "@lib/hooks";
|
||||
import { ContainerTerminal } from "@components/terminal/container";
|
||||
import { DeploymentInspect } from "./inspect";
|
||||
import { useMemo } from "react";
|
||||
import { DeploymentTabs } from "./tabs";
|
||||
|
||||
// const configOrLog = atomWithStorage("config-or-log-v1", "Config");
|
||||
|
||||
@@ -51,124 +45,6 @@ export const useFullDeployment = (id: string) =>
|
||||
useRead("GetDeployment", { deployment: id }, { refetchInterval: 10_000 })
|
||||
.data;
|
||||
|
||||
const ConfigTabs = ({ id }: { id: string }) => {
|
||||
const deployment = useDeployment(id);
|
||||
if (!deployment) return null;
|
||||
return <ConfigTabsInner deployment={deployment} />;
|
||||
};
|
||||
|
||||
const ConfigTabsInner = ({
|
||||
deployment,
|
||||
}: {
|
||||
deployment: Types.DeploymentListItem;
|
||||
}) => {
|
||||
// const [view, setView] = useAtom(configOrLog);
|
||||
const [_view, setView] = useLocalStorage<
|
||||
"Config" | "Log" | "Inspect" | "Terminal"
|
||||
>("deployment-tabs-v1", "Config");
|
||||
const { specificLogs, specificInspect, specificTerminal } = usePermissions({
|
||||
type: "Deployment",
|
||||
id: deployment.id,
|
||||
});
|
||||
const container_terminals_disabled =
|
||||
useServer(deployment.info.server_id)?.info.container_terminals_disabled ??
|
||||
true;
|
||||
const state = deployment.info.state;
|
||||
const logsDisabled =
|
||||
!specificLogs ||
|
||||
state === undefined ||
|
||||
state === Types.DeploymentState.Unknown ||
|
||||
state === Types.DeploymentState.NotDeployed;
|
||||
const inspectDisabled =
|
||||
!specificInspect ||
|
||||
state === undefined ||
|
||||
state === Types.DeploymentState.Unknown ||
|
||||
state === Types.DeploymentState.NotDeployed;
|
||||
const terminalDisabled =
|
||||
!specificTerminal ||
|
||||
container_terminals_disabled ||
|
||||
state !== Types.DeploymentState.Running;
|
||||
const view =
|
||||
(logsDisabled && _view === "Log") ||
|
||||
(inspectDisabled && _view === "Inspect") ||
|
||||
(terminalDisabled && _view === "Terminal")
|
||||
? "Config"
|
||||
: _view;
|
||||
|
||||
const tabs = useMemo(
|
||||
() => (
|
||||
<TabsList className="justify-start w-fit">
|
||||
<TabsTrigger value="Config" className="w-[110px]">
|
||||
Config
|
||||
</TabsTrigger>
|
||||
{specificLogs && (
|
||||
<TabsTrigger
|
||||
value="Log"
|
||||
className="w-[110px]"
|
||||
disabled={logsDisabled}
|
||||
>
|
||||
Log
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{specificInspect && (
|
||||
<TabsTrigger
|
||||
value="Inspect"
|
||||
className="w-[110px]"
|
||||
disabled={inspectDisabled}
|
||||
>
|
||||
Inspect
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{specificTerminal && (
|
||||
<TabsTrigger
|
||||
value="Terminal"
|
||||
className="w-[110px]"
|
||||
disabled={terminalDisabled}
|
||||
>
|
||||
Terminal
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
),
|
||||
[
|
||||
specificLogs,
|
||||
logsDisabled,
|
||||
specificInspect,
|
||||
inspectDisabled,
|
||||
specificTerminal,
|
||||
terminalDisabled,
|
||||
]
|
||||
);
|
||||
const terminalQuery = useMemo(
|
||||
() =>
|
||||
({
|
||||
type: "deployment",
|
||||
query: {
|
||||
deployment: deployment.id,
|
||||
// This is handled inside ContainerTerminal
|
||||
shell: "",
|
||||
},
|
||||
}) as ConnectExecQuery,
|
||||
[deployment.id]
|
||||
);
|
||||
return (
|
||||
<Tabs value={view} onValueChange={setView as any}>
|
||||
<TabsContent value="Config">
|
||||
<DeploymentConfig id={deployment.id} titleOther={tabs} />
|
||||
</TabsContent>
|
||||
<TabsContent value="Log">
|
||||
<DeploymentLogs id={deployment.id} titleOther={tabs} />
|
||||
</TabsContent>
|
||||
<TabsContent value="Inspect">
|
||||
<DeploymentInspect id={deployment.id} titleOther={tabs} />
|
||||
</TabsContent>
|
||||
<TabsContent value="Terminal">
|
||||
<ContainerTerminal query={terminalQuery} titleOther={tabs} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
const DeploymentIcon = ({ id, size }: { id?: string; size: number }) => {
|
||||
const state = useDeployment(id)?.info.state;
|
||||
const color = stroke_color_class_by_intention(
|
||||
@@ -233,6 +109,7 @@ export const DeploymentComponents: RequiredResourceComponents = {
|
||||
<NewResource
|
||||
type="Deployment"
|
||||
server_id={server_id}
|
||||
selectServer={!server_id}
|
||||
build_id={build_id}
|
||||
/>
|
||||
);
|
||||
@@ -359,7 +236,7 @@ export const DeploymentComponents: RequiredResourceComponents = {
|
||||
|
||||
Page: {},
|
||||
|
||||
Config: ConfigTabs,
|
||||
Config: DeploymentTabs,
|
||||
|
||||
DangerZone: ({ id }) => <DeleteResource type="Deployment" id={id} />,
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { ConnectExecQuery, Types } from "komodo_client";
|
||||
import { useDeployment } from ".";
|
||||
import { useLocalStorage, usePermissions } from "@lib/hooks";
|
||||
import { useServer } from "../server";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
MobileFriendlyTabsSelector,
|
||||
TabNoContent,
|
||||
} from "@ui/mobile-friendly-tabs";
|
||||
import { DeploymentConfig } from "./config";
|
||||
import { DeploymentLogs } from "./log";
|
||||
import { DeploymentInspect } from "./inspect";
|
||||
import { ContainerTerminal } from "@components/terminal/container";
|
||||
|
||||
export const DeploymentTabs = ({ id }: { id: string }) => {
|
||||
const deployment = useDeployment(id);
|
||||
if (!deployment) return null;
|
||||
return <DeploymentTabsInner deployment={deployment} />;
|
||||
};
|
||||
|
||||
type DeploymentTabsView = "Config" | "Log" | "Inspect" | "Terminal";
|
||||
|
||||
const DeploymentTabsInner = ({
|
||||
deployment,
|
||||
}: {
|
||||
deployment: Types.DeploymentListItem;
|
||||
}) => {
|
||||
const [_view, setView] = useLocalStorage<DeploymentTabsView>(
|
||||
"deployment-tabs-v1",
|
||||
"Config"
|
||||
);
|
||||
const { specificLogs, specificInspect, specificTerminal } = usePermissions({
|
||||
type: "Deployment",
|
||||
id: deployment.id,
|
||||
});
|
||||
const container_terminals_disabled =
|
||||
useServer(deployment.info.server_id)?.info.container_terminals_disabled ??
|
||||
true;
|
||||
const state = deployment.info.state;
|
||||
const logsDisabled =
|
||||
!specificLogs ||
|
||||
state === undefined ||
|
||||
state === Types.DeploymentState.Unknown ||
|
||||
state === Types.DeploymentState.NotDeployed;
|
||||
const inspectDisabled =
|
||||
!specificInspect ||
|
||||
state === undefined ||
|
||||
state === Types.DeploymentState.Unknown ||
|
||||
state === Types.DeploymentState.NotDeployed;
|
||||
const terminalDisabled =
|
||||
!specificTerminal ||
|
||||
container_terminals_disabled ||
|
||||
state !== Types.DeploymentState.Running;
|
||||
const view =
|
||||
(logsDisabled && _view === "Log") ||
|
||||
(inspectDisabled && _view === "Inspect") ||
|
||||
(terminalDisabled && _view === "Terminal")
|
||||
? "Config"
|
||||
: _view;
|
||||
|
||||
const tabsNoContent = useMemo<TabNoContent<DeploymentTabsView>[]>(
|
||||
() => [
|
||||
{
|
||||
value: "Config",
|
||||
},
|
||||
{
|
||||
value: "Log",
|
||||
hidden: !specificLogs,
|
||||
disabled: logsDisabled,
|
||||
},
|
||||
{
|
||||
value: "Inspect",
|
||||
hidden: !specificInspect,
|
||||
disabled: inspectDisabled,
|
||||
},
|
||||
{
|
||||
value: "Terminal",
|
||||
hidden: !specificTerminal,
|
||||
disabled: terminalDisabled,
|
||||
},
|
||||
],
|
||||
[
|
||||
specificLogs,
|
||||
logsDisabled,
|
||||
specificInspect,
|
||||
inspectDisabled,
|
||||
specificTerminal,
|
||||
terminalDisabled,
|
||||
]
|
||||
);
|
||||
|
||||
const Selector = (
|
||||
<MobileFriendlyTabsSelector
|
||||
tabs={tabsNoContent}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabsTriggerClassname="w-[110px]"
|
||||
/>
|
||||
);
|
||||
|
||||
const terminalQuery = useMemo(
|
||||
() =>
|
||||
({
|
||||
type: "deployment",
|
||||
query: {
|
||||
deployment: deployment.id,
|
||||
// This is handled inside ContainerTerminal
|
||||
shell: "",
|
||||
},
|
||||
}) as ConnectExecQuery,
|
||||
[deployment.id]
|
||||
);
|
||||
|
||||
switch (view) {
|
||||
case "Config":
|
||||
return <DeploymentConfig id={deployment.id} titleOther={Selector} />;
|
||||
case "Log":
|
||||
return <DeploymentLogs id={deployment.id} titleOther={Selector} />;
|
||||
case "Inspect":
|
||||
return <DeploymentInspect id={deployment.id} titleOther={Selector} />;
|
||||
case "Terminal":
|
||||
return <ContainerTerminal query={terminalQuery} titleOther={Selector} />;
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user