Compare commits

...
Author SHA1 Message Date
mbecker20 8c76b8487f alert responsive, better Server terminal disabled 2025-10-22 13:48:08 -07:00
mbecker20 2b32d9042a deploy 2.0.0-dev-72 2025-10-22 01:00:19 -07:00
mbecker20 dc48f1f2ca deploy 2.0.0-dev-71 2025-10-22 00:50:02 -07:00
mbecker20 8e7b7bdcf1 deploy 2.0.0-dev-70 2025-10-22 00:44:54 -07:00
mbecker20 f11d64f72e add 'init' param to make 'execute_terminal' in single call possible 2025-10-22 00:44:33 -07:00
mbecker20 2ffae85180 dashboard table section headers link to resources page 2025-10-22 00:03:12 -07:00
mbecker20 bd79d0f1e0 km ssh <SERVER> [COMMAND] -n [NAME] 2025-10-21 23:55:36 -07:00
mbecker20 e890b1f675 deploy 2.0.0-dev-69 2025-10-21 23:32:18 -07:00
mbecker20 3b7de25c30 Shift + X - Terminals, Shift + N - New (Resource, Terminal) 2025-10-21 16:11:27 -07:00
mbecker20 793bb99f31 nav to terminal on create 2025-10-21 16:00:50 -07:00
mbecker20 d465c9f273 deploy 2.0.0-dev-68 2025-10-21 15:51:38 -07:00
mbecker20 ce641a8974 terminal page 2025-10-21 15:51:18 -07:00
mbecker20 1b89ceb122 deploy 2.0.0-dev-67 2025-10-21 02:50:21 -07:00
mbecker20 2dbc011d26 remove unneeded log on client terminal disconnect 2025-10-21 02:33:19 -07:00
mbecker20 246da88ae1 deploy 2.0.0-dev-66 2025-10-21 02:29:12 -07:00
mbecker20 a8c16f64b1 km ssh 2025-10-21 02:28:42 -07:00
mbecker20 a5b711a348 stack tabs localstorage increment 2025-10-20 20:35:08 -07:00
mbecker20 9666e9ad83 Fix monitoring table with proper server version component 2025-10-20 03:01:07 -07:00
mbecker20 7479640c73 add hover information for mysterious server header icons 2025-10-20 02:53:18 -07:00
mbecker20 4823825035 give websocket indicator info on hover 2025-10-20 02:35:12 -07:00
mbecker20 23897a7acf clippy 2025-10-20 02:16:52 -07:00
mbecker20 20d5588b5c deploy 2.0.0-dev-65 2025-10-20 02:15:15 -07:00
mbecker20 f7e15ccde5 progress on terminals page 2025-10-20 02:14:51 -07:00
mbecker20 cf7623b1fc combine all resources / table view into dashboard 2025-10-20 01:40:27 -07:00
mbecker20 d3c464c05d start Terminals management page 2025-10-20 00:42:45 -07:00
mbecker20 5c9d416aa4 prog on docs update 2025-10-19 23:33:41 -07:00
mbecker20 aabcd88312 update connect-servers docs 2025-10-19 23:07:50 -07:00
mbecker20 9d2624c6bc clarify root directory in periphery config file 2025-10-19 23:07:19 -07:00
mbecker20 ee11fb0b6c clean up setup script 2025-10-19 23:07:02 -07:00
mbecker20 45adfbddd0 mounting custom CA 2025-10-19 23:06:48 -07:00
mbecker20 d26d035dc6 clean up docs intro 2025-10-19 22:03:17 -07:00
mbecker20 e673ba0adf deploy 2.0.0-dev-64 2025-10-19 21:48:15 -07:00
mbecker20 f876facfa7 improve git status message / failure propogation 2025-10-19 21:47:29 -07:00
mbecker20 3a47d57478 container class px-[1.2rem] 2025-10-19 20:31:40 -07:00
mbecker20 a707028277 responsive tweaks 2025-10-19 20:07:30 -07:00
mbecker20 0c6276c677 fix Resources / Containers mobile 2025-10-19 19:51:28 -07:00
mbecker20 fc9c6706f1 keep more descriptive settings header mobile 2025-10-19 13:24:44 -07:00
mbecker20 7674269ce9 fix user dropdown not showing username mobile 2025-10-19 13:11:34 -07:00
mbecker20 3b511c5adc improve server terminal mobile responsiveness 2025-10-19 13:00:30 -07:00
mbecker20 87221a10e9 fix mobile ContainerTerminal responsiveness 2025-10-19 12:56:11 -07:00
mbecker20 450cb6a148 fix stack config files mobile responsiveness 2025-10-19 12:46:51 -07:00
mbecker20 f252cefb21 responsive server docker tab 2025-10-19 12:37:26 -07:00
mbecker20 7855e9d688 run dkf 2025-10-19 12:30:59 -07:00
mbecker20 feb263c15f more type safe tabs 2025-10-19 12:27:55 -07:00
mbecker20 4f8d1c22cc rest of tabs also use mobile friendly 2025-10-19 12:11:11 -07:00
mbecker20 60bd47834e deploy 2.0.0-dev-63 2025-10-19 11:48:09 -07:00
mbecker20 4d632a6b61 improve resources mobile tabs responsiveness 2025-10-19 11:47:47 -07:00
mbecker20 381dd76723 deploy 2.0.0-dev-62 2025-10-19 01:37:10 -07:00
mbecker20 077e28a5fe fix ConfigList too wide on mobile 2025-10-19 01:36:50 -07:00
mbecker20 6b02aaed7d hide core pubkey copy if origin not https 2025-10-19 01:28:45 -07:00
mbecker20 e466944c05 improve mobile settings view 2025-10-19 01:24:41 -07:00
mbecker20 8ff94b7465 deploy 2.0.0-dev-61 2025-10-19 00:35:26 -07:00
mbecker20 b17df5ed7b show host public ip 2025-10-19 00:34:52 -07:00
mbecker20 207dc30206 cli is distroless, no shell / update-ca-certificates 2025-10-18 22:12:44 -07:00
mbecker20 c3eb386bdb fix copy entrypoint 2025-10-18 22:07:16 -07:00
mbecker20 4279e46892 deploy 2.0.0-dev-60 2025-10-18 12:59:19 -07:00
mbecker20 8d3d2fee12 use entrypoint scripts to make update-ca-certificates consistent when using custom CMD 2025-10-18 12:58:55 -07:00
158 changed files with 3973 additions and 2358 deletions
Generated
+229 -125
View File
@@ -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
View File
@@ -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"
+4
View File
@@ -0,0 +1,4 @@
const cmd = "km run -y action deploy-komodo-fe-change";
new Deno.Command("bash", {
args: ["-c", cmd],
}).spawn();
+3
View File
@@ -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
+74 -9
View File
@@ -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 {
+1
View File
@@ -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> {
+193
View File
@@ -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
View File
@@ -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
View File
@@ -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
+3
View File
@@ -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
+6 -2
View File
@@ -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"
+5 -1
View File
@@ -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 -1
View File
@@ -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,
+3 -1
View File
@@ -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::{
+1 -1
View File
@@ -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::{
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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::{
+1 -1
View File
@@ -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::{
+1
View File
@@ -114,6 +114,7 @@ enum ReadRequest {
ListServers(ListServers),
ListFullServers(ListFullServers),
ListTerminals(ListTerminals),
ListAllTerminals(ListAllTerminals),
// ==== DOCKER ====
GetDockerContainersSummary(GetDockerContainersSummary),
+1 -1
View File
@@ -1,4 +1,4 @@
use futures::future::join_all;
use futures_util::future::join_all;
use komodo_client::{
api::read::*,
entities::{
+74 -15
View File
@@ -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())
}
}
+35 -6
View File
@@ -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",
+48 -32
View File
@@ -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>,
}
+1
View File
@@ -92,6 +92,7 @@ pub enum WriteRequest {
CreateTerminal(CreateTerminal),
DeleteTerminal(DeleteTerminal),
DeleteAllTerminals(DeleteAllTerminals),
BatchDeleteAllTerminals(BatchDeleteAllTerminals),
UpdateServerPublicKey(UpdateServerPublicKey),
RotateServerKeys(RotateServerKeys),
+55
View File
@@ -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",
+2 -1
View File
@@ -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 {
+1 -1
View File
@@ -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::{
+1 -1
View File
@@ -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};
+1 -1
View File
@@ -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,
+56 -55
View File
@@ -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;
});
}
}
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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},
+1 -1
View File
@@ -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::{
+4 -1
View File
@@ -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,
},
+15 -3
View File
@@ -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,
})
}
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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},
+17 -4
View File
@@ -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(
+8 -5
View File
@@ -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
})
+8 -5
View File
@@ -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
View File
@@ -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;
}
}
+7 -4
View File
@@ -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
})
+3 -14
View File
@@ -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
})
}
+1 -1
View File
@@ -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,
};
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
## Update certificates.
update-ca-certificates
## Let the actual command take over
exec "$@"
+2 -1
View File
@@ -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
+5 -1
View File
@@ -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"
+5 -1
View File
@@ -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"
+5 -1
View File
@@ -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 -1
View File
@@ -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},
+5 -2
View File
@@ -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,
+40 -23
View File
@@ -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;
+3
View File
@@ -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),
+1 -1
View File
@@ -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::{
+54 -1
View File
@@ -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 -1
View File
@@ -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;
+17 -1
View File
@@ -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()
}
+28 -4
View File
@@ -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(),
})
}
+1 -2
View File
@@ -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
+25 -1
View File
@@ -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>;
+28 -22
View File
@@ -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>,
}
+20 -8
View File
@@ -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,
+48 -1
View File
@@ -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.
+4 -4
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
use futures::{Stream, StreamExt, TryStreamExt};
use futures_util::{Stream, StreamExt, TryStreamExt};
pub struct TerminalStreamResponse(pub reqwest::Response);
-288
View File
@@ -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))
}
}
+96
View File
@@ -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(_) => {}
};
}
}
}
+148
View File
@@ -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))
}
}
+2
View File
@@ -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
View File
@@ -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 }
+5 -8
View File
@@ -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,
}
//
+17 -7
View File
@@ -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")
})
}))
}
}
+4
View File
@@ -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
+4
View File
@@ -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
+2
View File
@@ -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:
+16 -3
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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).
+20 -1
View File
@@ -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
+1 -1
View File
@@ -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.
:::
+20 -9
View File
@@ -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
+4 -1
View File
@@ -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
View File
@@ -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;
+84 -11
View File
@@ -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;
+2 -2
View File
@@ -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>
),
+1 -1
View File
@@ -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
+41 -36
View File
@@ -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);
+24 -2
View File
@@ -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>
);
}
};
+64 -20
View File
@@ -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