mirror of
https://github.com/moghtech/komodo.git
synced 2025-12-05 19:17:36 -06:00
1.17.4 (#446)
* add terminal (ssh) apis * add core terminal exec method * terminal typescript client method * terminals WIP * backend for pty * add ts responses * about wire everything * add new blog * credit Skyfay * working * regen lock * 1.17.4-dev-1 * pty history * replace the test terminal impl with websocket (pty) * create api and improve frontend * fix fe * terminals * disable terminal api on periphery * implement write level terminal perms * remove unneeded * fix clippy * delete unneeded * fix waste cpu cycles * set TERM and COLORTERM for shell environment * fix xterm scrolling behavior * starship promp in periphery container terminal * kill all terminals on periphery shutdown signal * improve starship config and enable ssl in compose * use same scrollTop setter * fix periphery container distribution link * support custom command / args to init terminal * allow fully configurable init command * docker exec into container * add permissioning for container exec * add starship to core container * add delete all terminals * dev-2 * finished gen client * core need curl * hide Terminal trigger if disabled * 1.17.4
This commit is contained in:
374
Cargo.lock
generated
374
Cargo.lock
generated
@@ -24,7 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom 0.2.15",
|
||||
"getrandom 0.2.16",
|
||||
"once_cell",
|
||||
"version_check 0.9.5",
|
||||
"zerocopy 0.7.35",
|
||||
@@ -165,9 +165,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "aws-config"
|
||||
version = "1.6.1"
|
||||
version = "1.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c39646d1a6b51240a1a23bb57ea4eebede7e16fbc237fdc876980233dcecb4f"
|
||||
checksum = "b6fcc63c9860579e4cb396239570e979376e70aab79e496621748a09913f8b36"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -195,9 +195,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-credential-types"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4471bef4c22a06d2c7a1b6492493d3fdf24a805323109d6874f9c94d5906ac14"
|
||||
checksum = "687bc16bc431a8533fe0097c7f0182874767f920989d7260950172ae8e3c4465"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-runtime-api",
|
||||
@@ -217,9 +217,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.28.0"
|
||||
version = "0.28.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9f7720b74ed28ca77f90769a71fd8c637a0137f6fae4ae947e1050229cff57f"
|
||||
checksum = "bfa9b6986f250236c27e5a204062434a773a13243d2ffc2955f37bdba4c5c6a1"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"cc",
|
||||
@@ -230,9 +230,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-runtime"
|
||||
version = "1.5.6"
|
||||
version = "1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0aff45ffe35196e593ea3b9dd65b320e51e2dda95aff4390bc459e461d09c6ad"
|
||||
checksum = "6c4063282c69991e57faab9e5cb21ae557e59f5b0fb285c196335243df8dc25c"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-sigv4",
|
||||
@@ -246,7 +246,6 @@ dependencies = [
|
||||
"fastrand",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"tracing",
|
||||
@@ -255,9 +254,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-ec2"
|
||||
version = "1.121.1"
|
||||
version = "1.123.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1da132c5b212d7f3d0662dd42b618505424eba492b8290ef292f56e2d30ee51c"
|
||||
checksum = "b03632589dee533daf47c598819353995b20e4db42a84078fffff11730c3fbbd"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -279,9 +278,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sso"
|
||||
version = "1.64.0"
|
||||
version = "1.65.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02d4bdb0e5f80f0689e61c77ab678b2b9304af329616af38aef5b6b967b8e736"
|
||||
checksum = "8efec445fb78df585327094fcef4cad895b154b58711e504db7a93c41aa27151"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -302,9 +301,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-ssooidc"
|
||||
version = "1.65.0"
|
||||
version = "1.66.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acbbb3ce8da257aedbccdcb1aadafbbb6a5fe9adf445db0e1ea897bdc7e22d08"
|
||||
checksum = "5e49cca619c10e7b002dc8e66928ceed66ab7f56c1a3be86c5437bf2d8d89bba"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -325,9 +324,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sts"
|
||||
version = "1.65.0"
|
||||
version = "1.66.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96a78a8f50a1630db757b60f679c8226a8a70ee2ab5f5e6e51dc67f6c61c7cfd"
|
||||
checksum = "7420479eac0a53f776cc8f0d493841ffe58ad9d9783f3947be7265784471b47a"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -349,9 +348,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sigv4"
|
||||
version = "1.3.0"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69d03c3c05ff80d54ff860fe38c726f6f494c639ae975203a101335f223386db"
|
||||
checksum = "3503af839bd8751d0bdc5a46b9cac93a003a353e635b0c12cf2376b5b53e41ea"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-smithy-http",
|
||||
@@ -363,7 +362,6 @@ dependencies = [
|
||||
"hmac",
|
||||
"http 0.2.12",
|
||||
"http 1.3.1",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"sha2",
|
||||
"time",
|
||||
@@ -383,9 +381,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-http"
|
||||
version = "0.62.0"
|
||||
version = "0.62.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5949124d11e538ca21142d1fba61ab0a2a2c1bc3ed323cdb3e4b878bfb83166"
|
||||
checksum = "99335bec6cdc50a346fda1437f9fefe33abf8c99060739a546a16457f2862ca9"
|
||||
dependencies = [
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
@@ -395,7 +393,6 @@ dependencies = [
|
||||
"http 0.2.12",
|
||||
"http 1.3.1",
|
||||
"http-body 0.4.6",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
@@ -411,7 +408,7 @@ dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"h2 0.4.8",
|
||||
"h2 0.4.9",
|
||||
"http 0.2.12",
|
||||
"http 1.3.1",
|
||||
"http-body 0.4.6",
|
||||
@@ -441,12 +438,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-observability"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "445d065e76bc1ef54963db400319f1dd3ebb3e0a74af20f7f7630625b0cc7cc0"
|
||||
checksum = "9364d5989ac4dd918e5cc4c4bdcc61c9be17dcd2586ea7f69e348fc7c6cab393"
|
||||
dependencies = [
|
||||
"aws-smithy-runtime-api",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -461,9 +457,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-runtime"
|
||||
version = "1.8.1"
|
||||
version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0152749e17ce4d1b47c7747bdfec09dac1ccafdcbc741ebf9daa2a373356730f"
|
||||
checksum = "14302f06d1d5b7d333fd819943075b13d27c7700b414f574c3c35859bfb55d5e"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http",
|
||||
@@ -477,7 +473,6 @@ dependencies = [
|
||||
"http 1.3.1",
|
||||
"http-body 0.4.6",
|
||||
"http-body 1.0.1",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"tokio",
|
||||
@@ -486,9 +481,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-runtime-api"
|
||||
version = "1.7.4"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3da37cf5d57011cb1753456518ec76e31691f1f474b73934a284eb2a1c76510f"
|
||||
checksum = "a1e5d9e3a80a18afa109391fb5ad09c3daf887b516c6fd805a157c6ea7994a57"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-types",
|
||||
@@ -503,9 +498,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-types"
|
||||
version = "1.3.0"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "836155caafba616c0ff9b07944324785de2ab016141c3550bd1c07882f8cee8f"
|
||||
checksum = "40076bd09fadbc12d5e026ae080d0930defa606856186e31d83ccc6a255eeaf3"
|
||||
dependencies = [
|
||||
"base64-simd",
|
||||
"bytes",
|
||||
@@ -538,9 +533,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-types"
|
||||
version = "1.3.6"
|
||||
version = "1.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3873f8deed8927ce8d04487630dc9ff73193bab64742a61d050e57a68dec4125"
|
||||
checksum = "8a322fec39e4df22777ed3ad8ea868ac2f94cd15e1a55f6ee8d8d6305057689a"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-smithy-async",
|
||||
@@ -851,13 +846,13 @@ dependencies = [
|
||||
"ahash",
|
||||
"base64 0.22.1",
|
||||
"bitvec",
|
||||
"getrandom 0.2.15",
|
||||
"getrandom 0.2.16",
|
||||
"getrandom 0.3.2",
|
||||
"hex",
|
||||
"indexmap 2.9.0",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"rand 0.9.0",
|
||||
"rand 0.9.1",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"serde_json",
|
||||
@@ -898,7 +893,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cache"
|
||||
version = "1.17.3"
|
||||
version = "1.17.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"tokio",
|
||||
@@ -906,9 +901,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.18"
|
||||
version = "1.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c"
|
||||
checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
@@ -930,6 +925,12 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
@@ -995,9 +996,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.36"
|
||||
version = "4.5.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04"
|
||||
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -1005,9 +1006,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.36"
|
||||
version = "4.5.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5"
|
||||
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -1059,7 +1060,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "command"
|
||||
version = "1.17.3"
|
||||
version = "1.17.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"formatting",
|
||||
@@ -1089,7 +1090,7 @@ version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||
dependencies = [
|
||||
"getrandom 0.2.15",
|
||||
"getrandom 0.2.16",
|
||||
"once_cell",
|
||||
"tiny-keccak",
|
||||
]
|
||||
@@ -1256,15 +1257,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.8.0"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010"
|
||||
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.9"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
|
||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"pem-rfc7468",
|
||||
@@ -1294,9 +1295,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "derive-where"
|
||||
version = "1.2.7"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25"
|
||||
checksum = "2364b9aa47e460ce9bca6ac1777d14c98eef7e274eb077beed49f3adc94183ed"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1367,9 +1368,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.19"
|
||||
version = "0.99.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f"
|
||||
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
@@ -1428,6 +1429,12 @@ version = "0.15.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||
|
||||
[[package]]
|
||||
name = "downcast-rs"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
version = "1.0.5"
|
||||
@@ -1537,7 +1544,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "environment_file"
|
||||
version = "1.17.3"
|
||||
version = "1.17.4"
|
||||
dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
@@ -1589,6 +1596,17 @@ version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||
|
||||
[[package]]
|
||||
name = "filedescriptor"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"thiserror 1.0.69",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -1606,7 +1624,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "formatting"
|
||||
version = "1.17.3"
|
||||
version = "1.17.4"
|
||||
dependencies = [
|
||||
"serror",
|
||||
]
|
||||
@@ -1735,9 +1753,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.15"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
@@ -1768,7 +1786,7 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "git"
|
||||
version = "1.17.3"
|
||||
version = "1.17.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cache",
|
||||
@@ -1819,9 +1837,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.8"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2"
|
||||
checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
@@ -1973,13 +1991,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hostname"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba"
|
||||
checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"windows 0.52.0",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2089,7 +2107,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"h2 0.4.8",
|
||||
"h2 0.4.9",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"httparse",
|
||||
@@ -2420,7 +2438,7 @@ dependencies = [
|
||||
"socket2",
|
||||
"widestring",
|
||||
"windows-sys 0.48.0",
|
||||
"winreg",
|
||||
"winreg 0.50.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2505,7 +2523,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_cli"
|
||||
version = "1.17.3"
|
||||
version = "1.17.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2521,7 +2539,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_client"
|
||||
version = "1.17.3"
|
||||
version = "1.17.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2552,7 +2570,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_core"
|
||||
version = "1.17.3"
|
||||
version = "1.17.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -2593,7 +2611,7 @@ dependencies = [
|
||||
"ordered_hash_map",
|
||||
"partial_derive2",
|
||||
"periphery_client",
|
||||
"rand 0.9.0",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"resolver_api",
|
||||
@@ -2607,6 +2625,7 @@ dependencies = [
|
||||
"slack_client_rs",
|
||||
"svi",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tokio-util",
|
||||
"toml",
|
||||
"toml_pretty",
|
||||
@@ -2620,13 +2639,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_periphery"
|
||||
version = "1.17.3"
|
||||
version = "1.17.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
"axum",
|
||||
"axum-server",
|
||||
"bollard",
|
||||
"bytes",
|
||||
"cache",
|
||||
"clap",
|
||||
"command",
|
||||
@@ -2641,6 +2661,8 @@ dependencies = [
|
||||
"logger",
|
||||
"merge_config_files",
|
||||
"periphery_client",
|
||||
"portable-pty",
|
||||
"rand 0.9.1",
|
||||
"resolver_api",
|
||||
"response",
|
||||
"run_command",
|
||||
@@ -2652,6 +2674,7 @@ dependencies = [
|
||||
"svi",
|
||||
"sysinfo",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
@@ -2673,9 +2696,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.171"
|
||||
version = "0.2.172"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
|
||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
@@ -2689,9 +2712,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.11"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
|
||||
checksum = "c9627da5196e5d8ed0b0495e61e518847578da83483c37288316d9b2e03a7f72"
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
@@ -2732,7 +2755,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logger"
|
||||
version = "1.17.3"
|
||||
version = "1.17.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
@@ -2860,9 +2883,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.7"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430"
|
||||
checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
@@ -2976,6 +2999,18 @@ dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases 0.1.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "4.2.3"
|
||||
@@ -3095,7 +3130,7 @@ checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"getrandom 0.2.15",
|
||||
"getrandom 0.2.16",
|
||||
"http 1.3.1",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
@@ -3109,9 +3144,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-foundation"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925"
|
||||
checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
]
|
||||
@@ -3278,7 +3313,7 @@ dependencies = [
|
||||
"glob",
|
||||
"opentelemetry",
|
||||
"percent-encoding",
|
||||
"rand 0.9.0",
|
||||
"rand 0.9.1",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
@@ -3465,15 +3500,19 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "periphery_client"
|
||||
version = "1.17.3"
|
||||
version = "1.17.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
"reqwest",
|
||||
"resolver_api",
|
||||
"rustls 0.23.26",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_qs",
|
||||
"serror",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -3568,6 +3607,27 @@ dependencies = [
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-pty"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 1.3.2",
|
||||
"downcast-rs",
|
||||
"filedescriptor",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"nix",
|
||||
"serial2",
|
||||
"shared_library",
|
||||
"shell-words",
|
||||
"winapi",
|
||||
"winreg 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
@@ -3604,9 +3664,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.94"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -3641,7 +3701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"cfg_aliases 0.2.1",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
@@ -3656,13 +3716,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.10"
|
||||
version = "0.11.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc"
|
||||
checksum = "bcbafbbdbb0f638fe3f35f3c56739f77a8a1d070cb25603226c83339b391472b"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.2",
|
||||
"rand 0.9.0",
|
||||
"rand 0.9.1",
|
||||
"ring",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustls 0.23.26",
|
||||
@@ -3680,7 +3740,7 @@ version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"cfg_aliases 0.2.1",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
@@ -3722,13 +3782,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.0"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
|
||||
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.3",
|
||||
"zerocopy 0.8.24",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3757,7 +3816,7 @@ version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom 0.2.15",
|
||||
"getrandom 0.2.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3834,7 +3893,7 @@ dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.4.8",
|
||||
"h2 0.4.9",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
@@ -3861,11 +3920,13 @@ dependencies = [
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.2",
|
||||
"tokio-util",
|
||||
"tower 0.5.2",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots 0.26.8",
|
||||
"windows-registry",
|
||||
@@ -3885,9 +3946,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reqwest-middleware"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64e8975513bd9a7a43aad01030e79b3498e05db14e9d945df6483e8cf9b8c4c4"
|
||||
checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3907,7 +3968,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"futures",
|
||||
"getrandom 0.2.15",
|
||||
"getrandom 0.2.16",
|
||||
"http 1.3.1",
|
||||
"hyper 1.6.0",
|
||||
"parking_lot 0.11.2",
|
||||
@@ -3922,13 +3983,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reqwest-tracing"
|
||||
version = "0.5.6"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c88a8d9cfe3319b5adc10f3ffc3db75c7346837a1f857f8269f6361f3b2744"
|
||||
checksum = "d75b0eee96990cfb4c09545847385e89b2d2d2e571143d55264a05d77c713780"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"getrandom 0.2.15",
|
||||
"getrandom 0.2.16",
|
||||
"http 1.3.1",
|
||||
"matchit",
|
||||
"reqwest",
|
||||
@@ -3967,7 +4028,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "response"
|
||||
version = "1.17.3"
|
||||
version = "1.17.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -4003,7 +4064,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.15",
|
||||
"getrandom 0.2.16",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
@@ -4392,6 +4453,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_qs"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b417bedc008acbdf6d6b4bc482d29859924114bbe2650b7921fb68a261d0aa6"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.20"
|
||||
@@ -4467,6 +4539,17 @@ dependencies = [
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serial2"
|
||||
version = "0.2.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7d1d08630509d69f90eff4afcd02c3bd974d979225cbd815ff5942351b14375"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serror"
|
||||
version = "0.5.0"
|
||||
@@ -4521,6 +4604,22 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared_library"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shell-words"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -4529,9 +4628,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.2"
|
||||
version = "1.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
|
||||
checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -4751,7 +4850,7 @@ dependencies = [
|
||||
"memchr",
|
||||
"ntapi",
|
||||
"objc2-core-foundation",
|
||||
"windows 0.57.0",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4970,15 +5069,19 @@ checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"rustls 0.23.26",
|
||||
"rustls-native-certs 0.8.1",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.2",
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.14"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034"
|
||||
checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
@@ -5242,7 +5345,9 @@ dependencies = [
|
||||
"http 1.3.1",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.9.0",
|
||||
"rand 0.9.1",
|
||||
"rustls 0.23.26",
|
||||
"rustls-pki-types",
|
||||
"sha1",
|
||||
"thiserror 2.0.12",
|
||||
"utf-8",
|
||||
@@ -5382,7 +5487,7 @@ checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
|
||||
dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
"js-sys",
|
||||
"rand 0.9.0",
|
||||
"rand 0.9.1",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
@@ -5506,6 +5611,19 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-streams"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-timer"
|
||||
version = "0.2.5"
|
||||
@@ -5605,16 +5723,6 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
|
||||
dependencies = [
|
||||
"windows-core 0.52.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.57.0"
|
||||
@@ -5625,15 +5733,6 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.57.0"
|
||||
@@ -5970,13 +6069,22 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.4"
|
||||
version = "0.7.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36"
|
||||
checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
|
||||
15
Cargo.toml
15
Cargo.toml
@@ -8,7 +8,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.17.3"
|
||||
version = "1.17.4"
|
||||
edition = "2024"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -44,19 +44,19 @@ mungos = "3.2.0"
|
||||
svi = "1.0.1"
|
||||
|
||||
# ASYNC
|
||||
reqwest = { version = "0.12.15", default-features = false, features = ["json", "rustls-tls-native-roots"] }
|
||||
reqwest = { version = "0.12.15", default-features = false, features = ["json", "stream", "rustls-tls-native-roots"] }
|
||||
tokio = { version = "1.44.1", features = ["full"] }
|
||||
tokio-util = "0.7.14"
|
||||
tokio-util = { version = "0.7.14", features = ["io", "codec"] }
|
||||
futures = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
arc-swap = "1.7.1"
|
||||
|
||||
# SERVER
|
||||
tokio-tungstenite = { version = "0.26.2", features = ["rustls-tls-native-roots"] }
|
||||
axum-extra = { version = "0.10.0", features = ["typed-header"] }
|
||||
tower-http = { version = "0.6.2", features = ["fs", "cors"] }
|
||||
axum-server = { version = "0.7.2", features = ["tls-rustls"] }
|
||||
axum = { version = "0.8.1", features = ["ws", "json", "macros"] }
|
||||
tokio-tungstenite = "0.26.2"
|
||||
|
||||
# SER/DE
|
||||
ordered_hash_map = { version = "0.4.0", features = ["serde"] }
|
||||
@@ -64,6 +64,7 @@ serde = { version = "1.0.219", features = ["derive"] }
|
||||
strum = { version = "0.27.1", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
serde_yaml = "0.9.34"
|
||||
serde_qs = "0.14.0"
|
||||
toml = "0.8.20"
|
||||
|
||||
# ERROR
|
||||
@@ -80,7 +81,7 @@ opentelemetry = "0.29.0"
|
||||
tracing = "0.1.41"
|
||||
|
||||
# CONFIG
|
||||
clap = { version = "4.5.36", features = ["derive"] }
|
||||
clap = { version = "4.5.37", features = ["derive"] }
|
||||
dotenvy = "0.15.7"
|
||||
envy = "0.4.2"
|
||||
|
||||
@@ -95,10 +96,11 @@ base64 = "0.22.1"
|
||||
rustls = "0.23.26"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.8"
|
||||
rand = "0.9.0"
|
||||
rand = "0.9.1"
|
||||
hex = "0.4.3"
|
||||
|
||||
# SYSTEM
|
||||
portable-pty = "0.9.0"
|
||||
bollard = "0.18.1"
|
||||
sysinfo = "0.34.2"
|
||||
|
||||
@@ -121,4 +123,5 @@ dashmap = "6.1.0"
|
||||
wildcard = "0.3.0"
|
||||
colored = "3.0.0"
|
||||
regex = "1.11.1"
|
||||
bytes = "1.10.1"
|
||||
bson = "2.14.0"
|
||||
|
||||
@@ -38,6 +38,7 @@ slack.workspace = true
|
||||
svi.workspace = true
|
||||
# external
|
||||
aws-credential-types.workspace = true
|
||||
tokio-tungstenite.workspace = true
|
||||
ordered_hash_map.workspace = true
|
||||
english-to-cron.workspace = true
|
||||
openidconnect.workspace = true
|
||||
|
||||
@@ -24,10 +24,9 @@ RUN cd frontend && yarn link komodo_client && yarn && yarn build
|
||||
# Final Image
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
# Install Deps
|
||||
RUN apt update && \
|
||||
apt install -y git ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
COPY ./bin/core/starship.toml /config/starship.toml
|
||||
COPY ./bin/core/debian-deps.sh .
|
||||
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
|
||||
|
||||
# Setup an application directory
|
||||
WORKDIR /app
|
||||
|
||||
14
bin/core/debian-deps.sh
Normal file
14
bin/core/debian-deps.sh
Normal file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
## Core deps installer
|
||||
|
||||
apt-get update
|
||||
apt-get install -y git curl ca-certificates
|
||||
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Starship prompt
|
||||
curl -sS https://starship.rs/install.sh | sh -s -- --yes --bin-dir /usr/local/bin
|
||||
echo 'export STARSHIP_CONFIG=/config/starship.toml' >> /root/.bashrc
|
||||
echo 'eval "$(starship init bash)"' >> /root/.bashrc
|
||||
|
||||
@@ -15,10 +15,9 @@ FROM ${FRONTEND_IMAGE} AS frontend
|
||||
# Final Image
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
# Install Deps
|
||||
RUN apt update && \
|
||||
apt install -y git ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
COPY ./bin/core/starship.toml /config/starship.toml
|
||||
COPY ./bin/core/debian-deps.sh .
|
||||
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -16,10 +16,9 @@ RUN cd frontend && yarn link komodo_client && yarn && yarn build
|
||||
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
# Install Deps
|
||||
RUN apt update && \
|
||||
apt install -y git ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
COPY ./bin/core/starship.toml /config/starship.toml
|
||||
COPY ./bin/core/debian-deps.sh .
|
||||
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
|
||||
|
||||
# Copy
|
||||
COPY ./config/core.config.toml /config/config.toml
|
||||
|
||||
@@ -123,6 +123,7 @@ enum ReadRequest {
|
||||
ListDockerImages(ListDockerImages),
|
||||
ListDockerVolumes(ListDockerVolumes),
|
||||
ListComposeProjects(ListComposeProjects),
|
||||
ListTerminals(ListTerminals),
|
||||
|
||||
// ==== DEPLOYMENT ====
|
||||
GetDeploymentsSummary(GetDeploymentsSummary),
|
||||
|
||||
@@ -21,9 +21,11 @@ use komodo_client::{
|
||||
network::Network,
|
||||
volume::Volume,
|
||||
},
|
||||
komodo_timestamp,
|
||||
permission::PermissionLevel,
|
||||
server::{
|
||||
Server, ServerActionState, ServerListItem, ServerState,
|
||||
TerminalInfo,
|
||||
},
|
||||
stack::{Stack, StackServiceNames},
|
||||
stats::{SystemInformation, SystemProcess},
|
||||
@@ -45,7 +47,10 @@ use resolver_api::Resolve;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
helpers::{periphery_client, query::get_all_tags},
|
||||
helpers::{
|
||||
periphery_client,
|
||||
query::{get_all_tags, get_system_info},
|
||||
},
|
||||
resource,
|
||||
stack::compose_container_match_regex,
|
||||
state::{action_states, db_client, server_status_cache},
|
||||
@@ -198,16 +203,6 @@ impl Resolve<ReadArgs> for GetServerActionState {
|
||||
}
|
||||
}
|
||||
|
||||
// This protects the peripheries from spam requests
|
||||
const SYSTEM_INFO_EXPIRY: u128 = FIFTEEN_SECONDS_MS;
|
||||
type SystemInfoCache =
|
||||
Mutex<HashMap<String, Arc<(SystemInformation, u128)>>>;
|
||||
fn system_info_cache() -> &'static SystemInfoCache {
|
||||
static SYSTEM_INFO_CACHE: OnceLock<SystemInfoCache> =
|
||||
OnceLock::new();
|
||||
SYSTEM_INFO_CACHE.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetSystemInformation {
|
||||
async fn resolve(
|
||||
self,
|
||||
@@ -219,25 +214,7 @@ impl Resolve<ReadArgs> for GetSystemInformation {
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut lock = system_info_cache().lock().await;
|
||||
let res = match lock.get(&server.id) {
|
||||
Some(cached) if cached.1 > unix_timestamp_ms() => {
|
||||
cached.0.clone()
|
||||
}
|
||||
_ => {
|
||||
let stats = periphery_client(&server)?
|
||||
.request(periphery::stats::GetSystemInformation {})
|
||||
.await?;
|
||||
lock.insert(
|
||||
server.id,
|
||||
(stats.clone(), unix_timestamp_ms() + SYSTEM_INFO_EXPIRY)
|
||||
.into(),
|
||||
);
|
||||
stats
|
||||
}
|
||||
};
|
||||
Ok(res)
|
||||
get_system_info(&server).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -812,3 +789,66 @@ impl Resolve<ReadArgs> for ListComposeProjects {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TerminalCacheItem {
|
||||
list: Vec<TerminalInfo>,
|
||||
ttl: i64,
|
||||
}
|
||||
|
||||
const TERMINAL_CACHE_TIMEOUT: i64 = 30_000;
|
||||
|
||||
#[derive(Default)]
|
||||
struct TerminalCache(
|
||||
std::sync::Mutex<
|
||||
HashMap<String, Arc<tokio::sync::Mutex<TerminalCacheItem>>>,
|
||||
>,
|
||||
);
|
||||
|
||||
impl TerminalCache {
|
||||
fn get_or_insert(
|
||||
&self,
|
||||
server_id: String,
|
||||
) -> Arc<tokio::sync::Mutex<TerminalCacheItem>> {
|
||||
if let Some(cached) =
|
||||
self.0.lock().unwrap().get(&server_id).cloned()
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
let to_cache =
|
||||
Arc::new(tokio::sync::Mutex::new(TerminalCacheItem::default()));
|
||||
self.0.lock().unwrap().insert(server_id, to_cache.clone());
|
||||
to_cache
|
||||
}
|
||||
}
|
||||
|
||||
fn terminals_cache() -> &'static TerminalCache {
|
||||
static TERMINALS: OnceLock<TerminalCache> = OnceLock::new();
|
||||
TERMINALS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListTerminals {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListTerminalsResponse> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.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)?
|
||||
.request(periphery_client::api::terminal::ListTerminals {})
|
||||
.await
|
||||
.context("Failed to get fresh terminal list")?;
|
||||
cache.ttl = komodo_timestamp() + TERMINAL_CACHE_TIMEOUT;
|
||||
Ok(cache.list.clone())
|
||||
} else {
|
||||
Ok(cache.list.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,9 @@ pub enum WriteRequest {
|
||||
UpdateServer(UpdateServer),
|
||||
RenameServer(RenameServer),
|
||||
CreateNetwork(CreateNetwork),
|
||||
CreateTerminal(CreateTerminal),
|
||||
DeleteTerminal(DeleteTerminal),
|
||||
DeleteAllTerminals(DeleteAllTerminals),
|
||||
|
||||
// ==== DEPLOYMENT ====
|
||||
CreateDeployment(CreateDeployment),
|
||||
@@ -208,10 +211,6 @@ async fn handler(
|
||||
.await
|
||||
.context("failure in spawned task");
|
||||
|
||||
if let Err(e) = &res {
|
||||
warn!("/write request {req_id} spawn error: {e:#}");
|
||||
}
|
||||
|
||||
res?
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use anyhow::Context;
|
||||
use formatting::format_serror;
|
||||
use komodo_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
Operation,
|
||||
NoData, Operation,
|
||||
permission::PermissionLevel,
|
||||
server::Server,
|
||||
update::{Update, UpdateStatus},
|
||||
@@ -101,3 +102,81 @@ impl Resolve<WriteArgs> for CreateNetwork {
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for CreateTerminal {
|
||||
#[instrument(name = "CreateTerminal", skip(user))]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<NoData> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
periphery
|
||||
.request(api::terminal::CreateTerminal {
|
||||
name: self.name,
|
||||
command: self.command,
|
||||
recreate: self.recreate,
|
||||
})
|
||||
.await
|
||||
.context("Failed to create terminal on periphery")?;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for DeleteTerminal {
|
||||
#[instrument(name = "DeleteTerminal", skip(user))]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<NoData> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
periphery
|
||||
.request(api::terminal::DeleteTerminal {
|
||||
terminal: self.terminal,
|
||||
})
|
||||
.await
|
||||
.context("Failed to delete terminal on periphery")?;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for DeleteAllTerminals {
|
||||
#[instrument(name = "DeleteAllTerminals", skip(user))]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<NoData> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
periphery
|
||||
.request(api::terminal::DeleteAllTerminals {})
|
||||
.await
|
||||
.context("Failed to delete all terminals on periphery")?;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
str::FromStr,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use async_timing_util::{ONE_MIN_MS, unix_timestamp_ms};
|
||||
use komodo_client::entities::{
|
||||
Operation, ResourceTarget, ResourceTargetVariant,
|
||||
action::Action,
|
||||
@@ -15,6 +20,7 @@ use komodo_client::entities::{
|
||||
server::{Server, ServerState},
|
||||
server_template::ServerTemplate,
|
||||
stack::{Stack, StackServiceNames, StackState},
|
||||
stats::SystemInformation,
|
||||
sync::ResourceSync,
|
||||
tag::Tag,
|
||||
update::Update,
|
||||
@@ -29,6 +35,8 @@ use mungos::{
|
||||
options::FindOneOptions,
|
||||
},
|
||||
};
|
||||
use periphery_client::api::stats;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
@@ -37,6 +45,8 @@ use crate::{
|
||||
state::{db_client, deployment_status_cache, stack_status_cache},
|
||||
};
|
||||
|
||||
use super::periphery_client;
|
||||
|
||||
// user: Id or username
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn get_user(user: &str) -> anyhow::Result<User> {
|
||||
@@ -382,3 +392,36 @@ pub async fn get_variables_and_secrets()
|
||||
|
||||
Ok(VariablesAndSecrets { variables, secrets })
|
||||
}
|
||||
|
||||
// This protects the peripheries from spam requests
|
||||
const SYSTEM_INFO_EXPIRY: u128 = ONE_MIN_MS;
|
||||
type SystemInfoCache =
|
||||
Mutex<HashMap<String, Arc<(SystemInformation, u128)>>>;
|
||||
fn system_info_cache() -> &'static SystemInfoCache {
|
||||
static SYSTEM_INFO_CACHE: OnceLock<SystemInfoCache> =
|
||||
OnceLock::new();
|
||||
SYSTEM_INFO_CACHE.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub async fn get_system_info(
|
||||
server: &Server,
|
||||
) -> anyhow::Result<SystemInformation> {
|
||||
let mut lock = system_info_cache().lock().await;
|
||||
let res = match lock.get(&server.id) {
|
||||
Some(cached) if cached.1 > unix_timestamp_ms() => {
|
||||
cached.0.clone()
|
||||
}
|
||||
_ => {
|
||||
let stats = periphery_client(server)?
|
||||
.request(stats::GetSystemInformation {})
|
||||
.await?;
|
||||
lock.insert(
|
||||
server.id.clone(),
|
||||
(stats.clone(), unix_timestamp_ms() + SYSTEM_INFO_EXPIRY)
|
||||
.into(),
|
||||
);
|
||||
stats
|
||||
}
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,12 @@ async fn app() -> anyhow::Result<()> {
|
||||
dotenvy::dotenv().ok();
|
||||
let config = core_config();
|
||||
logger::init(&config.logging)?;
|
||||
if let Err(e) =
|
||||
rustls::crypto::aws_lc_rs::default_provider().install_default()
|
||||
{
|
||||
error!("Failed to install default crypto provider | {e:?}");
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
info!("Komodo Core version: v{}", env!("CARGO_PKG_VERSION"));
|
||||
info!("{:?}", config.sanitized());
|
||||
|
||||
@@ -13,6 +13,7 @@ use mungos::mongodb::{Collection, bson::doc};
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::query::get_system_info,
|
||||
monitor::update_cache_for_server,
|
||||
state::{action_states, db_client, server_status_cache},
|
||||
};
|
||||
@@ -42,6 +43,10 @@ impl super::KomodoResource for Server {
|
||||
server: Resource<Self::Config, Self::Info>,
|
||||
) -> Self::ListItem {
|
||||
let status = server_status_cache().get(&server.id).await;
|
||||
let terminals_disabled = get_system_info(&server)
|
||||
.await
|
||||
.map(|i| i.terminals_disabled)
|
||||
.unwrap_or(true);
|
||||
ServerListItem {
|
||||
name: server.name,
|
||||
id: server.id,
|
||||
@@ -57,6 +62,7 @@ impl super::KomodoResource for Server {
|
||||
send_cpu_alerts: server.config.send_cpu_alerts,
|
||||
send_mem_alerts: server.config.send_mem_alerts,
|
||||
send_disk_alerts: server.config.send_disk_alerts,
|
||||
terminals_disabled,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{
|
||||
WebSocketUpgrade,
|
||||
ws::{Message, WebSocket},
|
||||
},
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use komodo_client::{
|
||||
entities::{
|
||||
ResourceTarget, permission::PermissionLevel, user::User,
|
||||
},
|
||||
ws::WsLoginMessage,
|
||||
};
|
||||
use serde_json::json;
|
||||
use serror::serialize_error;
|
||||
use tokio::select;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{
|
||||
auth::{auth_api_key_check_enabled, auth_jwt_check_enabled},
|
||||
helpers::{
|
||||
channel::update_channel,
|
||||
query::{get_user, get_user_permission_on_target},
|
||||
},
|
||||
};
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new().route("/update", get(ws_handler))
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn ws_handler(ws: WebSocketUpgrade) -> impl IntoResponse {
|
||||
// get a reveiver for internal update messages.
|
||||
let mut receiver = update_channel().receiver.resubscribe();
|
||||
|
||||
// handle http -> ws updgrade
|
||||
ws.on_upgrade(|socket| async move {
|
||||
let Some((socket, user)) = ws_login(socket).await else {
|
||||
return
|
||||
};
|
||||
|
||||
let (mut ws_sender, mut ws_reciever) = socket.split();
|
||||
|
||||
let cancel = CancellationToken::new();
|
||||
let cancel_clone = cancel.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
// poll for updates off the receiver / await cancel.
|
||||
let update = select! {
|
||||
_ = cancel_clone.cancelled() => break,
|
||||
update = receiver.recv() => {update.expect("failed to recv update msg")}
|
||||
};
|
||||
|
||||
// before sending every update, verify user is still valid.
|
||||
// kill the connection is user if found to be invalid.
|
||||
let user = check_user_valid(&user.id).await;
|
||||
let user = match user {
|
||||
Err(e) => {
|
||||
let _ = ws_sender
|
||||
.send(Message::text(json!({ "type": "INVALID_USER", "msg": serialize_error(&e) }).to_string()))
|
||||
.await;
|
||||
let _ = ws_sender.close().await;
|
||||
return;
|
||||
},
|
||||
Ok(user) => user,
|
||||
};
|
||||
|
||||
// Only send if user has permission on the target resource.
|
||||
if user_can_see_update(&user, &update.target).await.is_ok() {
|
||||
let _ = ws_sender
|
||||
.send(Message::text(serde_json::to_string(&update).unwrap()))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle messages from the client.
|
||||
// After login, only handles close message.
|
||||
while let Some(msg) = ws_reciever.next().await {
|
||||
match msg {
|
||||
Ok(msg) => {
|
||||
if let Message::Close(_) = msg {
|
||||
cancel.cancel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
cancel.cancel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn ws_login(
|
||||
mut socket: WebSocket,
|
||||
) -> Option<(WebSocket, User)> {
|
||||
let login_msg = match socket.recv().await {
|
||||
Some(Ok(Message::Text(login_msg))) => {
|
||||
LoginMessage::Ok(login_msg.to_string())
|
||||
}
|
||||
Some(Ok(msg)) => {
|
||||
LoginMessage::Err(format!("invalid login message: {msg:?}"))
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
LoginMessage::Err(format!("failed to get login message: {e:?}"))
|
||||
}
|
||||
None => {
|
||||
LoginMessage::Err("failed to get login message".to_string())
|
||||
}
|
||||
};
|
||||
let login_msg = match login_msg {
|
||||
LoginMessage::Ok(login_msg) => login_msg,
|
||||
LoginMessage::Err(msg) => {
|
||||
let _ = socket.send(Message::text(msg)).await;
|
||||
let _ = socket.close().await;
|
||||
return None;
|
||||
}
|
||||
};
|
||||
match WsLoginMessage::from_json_str(&login_msg) {
|
||||
// Login using a jwt
|
||||
Ok(WsLoginMessage::Jwt { jwt }) => {
|
||||
match auth_jwt_check_enabled(&jwt).await {
|
||||
Ok(user) => {
|
||||
let _ = socket.send(Message::text("LOGGED_IN")).await;
|
||||
Some((socket, user))
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = socket
|
||||
.send(Message::text(format!(
|
||||
"failed to authenticate user using jwt | {e:#}"
|
||||
)))
|
||||
.await;
|
||||
let _ = socket.close().await;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
// login using api keys
|
||||
Ok(WsLoginMessage::ApiKeys { key, secret }) => {
|
||||
match auth_api_key_check_enabled(&key, &secret).await {
|
||||
Ok(user) => {
|
||||
let _ = socket.send(Message::text("LOGGED_IN")).await;
|
||||
Some((socket, user))
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = socket
|
||||
.send(Message::text(format!(
|
||||
"failed to authenticate user using api keys | {e:#}"
|
||||
)))
|
||||
.await;
|
||||
let _ = socket.close().await;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = socket
|
||||
.send(Message::text(format!(
|
||||
"failed to parse login message: {e:#}"
|
||||
)))
|
||||
.await;
|
||||
let _ = socket.close().await;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum LoginMessage {
|
||||
/// The text message
|
||||
Ok(String),
|
||||
/// The err message
|
||||
Err(String),
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn check_user_valid(user_id: &str) -> anyhow::Result<User> {
|
||||
let user = get_user(user_id).await?;
|
||||
if !user.enabled {
|
||||
return Err(anyhow!("user not enabled"));
|
||||
}
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn user_can_see_update(
|
||||
user: &User,
|
||||
update_target: &ResourceTarget,
|
||||
) -> anyhow::Result<()> {
|
||||
if user.admin {
|
||||
return Ok(());
|
||||
}
|
||||
let permissions =
|
||||
get_user_permission_on_target(user, update_target).await?;
|
||||
if permissions > PermissionLevel::None {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"user does not have permissions on {update_target:?}"
|
||||
))
|
||||
}
|
||||
}
|
||||
112
bin/core/src/ws/mod.rs
Normal file
112
bin/core/src/ws/mod.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use crate::{
|
||||
auth::{auth_api_key_check_enabled, auth_jwt_check_enabled},
|
||||
helpers::query::get_user,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use axum::{
|
||||
Router,
|
||||
extract::ws::{Message, WebSocket},
|
||||
routing::get,
|
||||
};
|
||||
use futures::SinkExt;
|
||||
use komodo_client::{entities::user::User, ws::WsLoginMessage};
|
||||
|
||||
mod terminal;
|
||||
mod update;
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/update", get(update::handler))
|
||||
.route("/terminal", get(terminal::handler))
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn ws_login(
|
||||
mut socket: WebSocket,
|
||||
) -> Option<(WebSocket, User)> {
|
||||
let login_msg = match socket.recv().await {
|
||||
Some(Ok(Message::Text(login_msg))) => {
|
||||
LoginMessage::Ok(login_msg.to_string())
|
||||
}
|
||||
Some(Ok(msg)) => {
|
||||
LoginMessage::Err(format!("invalid login message: {msg:?}"))
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
LoginMessage::Err(format!("failed to get login message: {e:?}"))
|
||||
}
|
||||
None => {
|
||||
LoginMessage::Err("failed to get login message".to_string())
|
||||
}
|
||||
};
|
||||
let login_msg = match login_msg {
|
||||
LoginMessage::Ok(login_msg) => login_msg,
|
||||
LoginMessage::Err(msg) => {
|
||||
let _ = socket.send(Message::text(msg)).await;
|
||||
let _ = socket.close().await;
|
||||
return None;
|
||||
}
|
||||
};
|
||||
match WsLoginMessage::from_json_str(&login_msg) {
|
||||
// Login using a jwt
|
||||
Ok(WsLoginMessage::Jwt { jwt }) => {
|
||||
match auth_jwt_check_enabled(&jwt).await {
|
||||
Ok(user) => {
|
||||
let _ = socket.send(Message::text("LOGGED_IN")).await;
|
||||
Some((socket, user))
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = socket
|
||||
.send(Message::text(format!(
|
||||
"failed to authenticate user using jwt | {e:#}"
|
||||
)))
|
||||
.await;
|
||||
let _ = socket.close().await;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
// login using api keys
|
||||
Ok(WsLoginMessage::ApiKeys { key, secret }) => {
|
||||
match auth_api_key_check_enabled(&key, &secret).await {
|
||||
Ok(user) => {
|
||||
let _ = socket.send(Message::text("LOGGED_IN")).await;
|
||||
Some((socket, user))
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = socket
|
||||
.send(Message::text(format!(
|
||||
"failed to authenticate user using api keys | {e:#}"
|
||||
)))
|
||||
.await;
|
||||
let _ = socket.close().await;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = socket
|
||||
.send(Message::text(format!(
|
||||
"failed to parse login message: {e:#}"
|
||||
)))
|
||||
.await;
|
||||
let _ = socket.close().await;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum LoginMessage {
|
||||
/// The text message
|
||||
Ok(String),
|
||||
/// The err message
|
||||
Err(String),
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn check_user_valid(user_id: &str) -> anyhow::Result<User> {
|
||||
let user = get_user(user_id).await?;
|
||||
if !user.enabled {
|
||||
return Err(anyhow!("user not enabled"));
|
||||
}
|
||||
Ok(user)
|
||||
}
|
||||
200
bin/core/src/ws/terminal.rs
Normal file
200
bin/core/src/ws/terminal.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
use axum::{
|
||||
extract::{
|
||||
Query, WebSocketUpgrade,
|
||||
ws::{CloseFrame, Message, Utf8Bytes},
|
||||
},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use komodo_client::{
|
||||
api::terminal::ConnectTerminalQuery,
|
||||
entities::{permission::PermissionLevel, server::Server},
|
||||
};
|
||||
use tokio_tungstenite::tungstenite;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{helpers::periphery_client, resource};
|
||||
|
||||
#[instrument(name = "ConnectTerminal", skip(ws))]
|
||||
pub async fn handler(
|
||||
Query(ConnectTerminalQuery {
|
||||
server,
|
||||
terminal,
|
||||
init,
|
||||
}): Query<ConnectTerminalQuery>,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(|socket| async move {
|
||||
let Some((mut socket, user)) = super::ws_login(socket).await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let server = match resource::get_check_permissions::<Server>(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(server) => server,
|
||||
Err(e) => {
|
||||
debug!("could not get server | {e:#}");
|
||||
let _ =
|
||||
socket.send(Message::text(format!("ERROR: {e:#}"))).await;
|
||||
let _ = socket.close().await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let periphery = match periphery_client(&server) {
|
||||
Ok(periphery) => periphery,
|
||||
Err(e) => {
|
||||
debug!("couldn't get periphery | {e:#}");
|
||||
let _ =
|
||||
socket.send(Message::text(format!("ERROR: {e:#}"))).await;
|
||||
let _ = socket.close().await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
trace!("connecting to periphery terminal");
|
||||
|
||||
let periphery_socket = match periphery
|
||||
.connect_terminal(
|
||||
terminal,
|
||||
init,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(ws) => ws,
|
||||
Err(e) => {
|
||||
debug!("Failed connect to periphery terminal | {e:#}");
|
||||
let _ =
|
||||
socket.send(Message::text(format!("ERROR: {e:#}"))).await;
|
||||
let _ = socket.close().await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
trace!("connected to periphery terminal socket");
|
||||
|
||||
let (mut periphery_send, mut periphery_receive) =
|
||||
periphery_socket.split();
|
||||
let (mut core_send, mut core_receive) = socket.split();
|
||||
let cancel = CancellationToken::new();
|
||||
|
||||
trace!("starting ws exchange");
|
||||
|
||||
let core_to_periphery = async {
|
||||
loop {
|
||||
let res = tokio::select! {
|
||||
res = core_receive.next() => res,
|
||||
_ = cancel.cancelled() => {
|
||||
trace!("core to periphery read: cancelled from inside");
|
||||
break;
|
||||
}
|
||||
};
|
||||
match res {
|
||||
Some(Ok(msg)) => {
|
||||
if let Err(e) =
|
||||
periphery_send.send(axum_to_tungstenite(msg)).await
|
||||
{
|
||||
debug!("Failed to send terminal message to {} | {e:?}", server.name);
|
||||
cancel.cancel();
|
||||
break;
|
||||
};
|
||||
}
|
||||
Some(Err(_e)) => {
|
||||
cancel.cancel();
|
||||
break;
|
||||
}
|
||||
None => {
|
||||
cancel.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let periphery_to_core = async {
|
||||
loop {
|
||||
let res = tokio::select! {
|
||||
res = periphery_receive.next() => res,
|
||||
_ = cancel.cancelled() => {
|
||||
trace!("periphery to core read: cancelled from inside");
|
||||
break;
|
||||
}
|
||||
};
|
||||
match res {
|
||||
Some(Ok(msg)) => {
|
||||
if let Err(e) =
|
||||
core_send.send(tungstenite_to_axum(msg)).await
|
||||
{
|
||||
debug!("{e:?}");
|
||||
cancel.cancel();
|
||||
break;
|
||||
};
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
let _ = core_send
|
||||
.send(Message::text(format!(
|
||||
"ERROR: Failed to receive message from periphery | {e:?}"
|
||||
)))
|
||||
.await;
|
||||
cancel.cancel();
|
||||
break;
|
||||
}
|
||||
None => {
|
||||
let _ = core_send
|
||||
.send(Message::text("STREAM EOF"))
|
||||
.await;
|
||||
cancel.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tokio::join!(core_to_periphery, periphery_to_core);
|
||||
})
|
||||
}
|
||||
|
||||
fn axum_to_tungstenite(msg: Message) -> tungstenite::Message {
|
||||
match msg {
|
||||
Message::Text(text) => tungstenite::Message::Text(
|
||||
tungstenite::Utf8Bytes::from(text.to_string()),
|
||||
),
|
||||
Message::Binary(bytes) => tungstenite::Message::Binary(bytes),
|
||||
Message::Ping(bytes) => tungstenite::Message::Ping(bytes),
|
||||
Message::Pong(bytes) => tungstenite::Message::Pong(bytes),
|
||||
Message::Close(close_frame) => {
|
||||
tungstenite::Message::Close(close_frame.map(|cf| {
|
||||
tungstenite::protocol::CloseFrame {
|
||||
code: cf.code.into(),
|
||||
reason: tungstenite::Utf8Bytes::from(cf.reason.to_string()),
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn tungstenite_to_axum(msg: tungstenite::Message) -> Message {
|
||||
match msg {
|
||||
tungstenite::Message::Text(text) => {
|
||||
Message::Text(Utf8Bytes::from(text.to_string()))
|
||||
}
|
||||
tungstenite::Message::Binary(bytes) => Message::Binary(bytes),
|
||||
tungstenite::Message::Ping(bytes) => Message::Ping(bytes),
|
||||
tungstenite::Message::Pong(bytes) => Message::Pong(bytes),
|
||||
tungstenite::Message::Close(close_frame) => {
|
||||
Message::Close(close_frame.map(|cf| CloseFrame {
|
||||
code: cf.code.into(),
|
||||
reason: Utf8Bytes::from(cf.reason.to_string()),
|
||||
}))
|
||||
}
|
||||
tungstenite::Message::Frame(_) => {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
102
bin/core/src/ws/update.rs
Normal file
102
bin/core/src/ws/update.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use anyhow::anyhow;
|
||||
use axum::{
|
||||
extract::{WebSocketUpgrade, ws::Message},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use komodo_client::entities::{
|
||||
ResourceTarget, permission::PermissionLevel, user::User,
|
||||
};
|
||||
use serde_json::json;
|
||||
use serror::serialize_error;
|
||||
use tokio::select;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::helpers::{
|
||||
channel::update_channel, query::get_user_permission_on_target,
|
||||
};
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn handler(ws: WebSocketUpgrade) -> impl IntoResponse {
|
||||
// get a reveiver for internal update messages.
|
||||
let mut receiver = update_channel().receiver.resubscribe();
|
||||
|
||||
// handle http -> ws updgrade
|
||||
ws.on_upgrade(|socket| async move {
|
||||
let Some((socket, user)) = super::ws_login(socket).await else {
|
||||
return
|
||||
};
|
||||
|
||||
let (mut ws_sender, mut ws_reciever) = socket.split();
|
||||
|
||||
let cancel = CancellationToken::new();
|
||||
let cancel_clone = cancel.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
// poll for updates off the receiver / await cancel.
|
||||
let update = select! {
|
||||
_ = cancel_clone.cancelled() => break,
|
||||
update = receiver.recv() => {update.expect("failed to recv update msg")}
|
||||
};
|
||||
|
||||
// before sending every update, verify user is still valid.
|
||||
// kill the connection is user if found to be invalid.
|
||||
let user = super::check_user_valid(&user.id).await;
|
||||
let user = match user {
|
||||
Err(e) => {
|
||||
let _ = ws_sender
|
||||
.send(Message::text(json!({ "type": "INVALID_USER", "msg": serialize_error(&e) }).to_string()))
|
||||
.await;
|
||||
let _ = ws_sender.close().await;
|
||||
return;
|
||||
},
|
||||
Ok(user) => user,
|
||||
};
|
||||
|
||||
// Only send if user has permission on the target resource.
|
||||
if user_can_see_update(&user, &update.target).await.is_ok() {
|
||||
let _ = ws_sender
|
||||
.send(Message::text(serde_json::to_string(&update).unwrap()))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle messages from the client.
|
||||
// After login, only handles close message.
|
||||
while let Some(msg) = ws_reciever.next().await {
|
||||
match msg {
|
||||
Ok(msg) => {
|
||||
if let Message::Close(_) = msg {
|
||||
cancel.cancel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
cancel.cancel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn user_can_see_update(
|
||||
user: &User,
|
||||
update_target: &ResourceTarget,
|
||||
) -> anyhow::Result<()> {
|
||||
if user.admin {
|
||||
return Ok(());
|
||||
}
|
||||
let permissions =
|
||||
get_user_permission_on_target(user, update_target).await?;
|
||||
if permissions > PermissionLevel::None {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"user does not have permissions on {update_target:?}"
|
||||
))
|
||||
}
|
||||
}
|
||||
67
bin/core/starship.toml
Normal file
67
bin/core/starship.toml
Normal file
@@ -0,0 +1,67 @@
|
||||
## This is used to customize the shell prompt in Periphery container for Terminals
|
||||
|
||||
"$schema" = 'https://starship.rs/config-schema.json'
|
||||
|
||||
add_newline = true
|
||||
|
||||
format = "$time$hostname$container$memory_usage$all"
|
||||
|
||||
[character]
|
||||
success_symbol = "[❯](bright-blue bold)"
|
||||
error_symbol = "[❯](bright-red bold)"
|
||||
|
||||
[package]
|
||||
disabled = true
|
||||
|
||||
[time]
|
||||
format = "[❯$time](white dimmed) "
|
||||
time_format = "%l:%M %p"
|
||||
utc_time_offset = '-5'
|
||||
disabled = true
|
||||
|
||||
[username]
|
||||
format = "[❯ $user]($style) "
|
||||
style_user = "bright-green"
|
||||
show_always = true
|
||||
|
||||
[hostname]
|
||||
format = "[❯ $hostname]($style) "
|
||||
style = "bright-blue"
|
||||
ssh_only = false
|
||||
|
||||
[directory]
|
||||
format = "[❯ $path]($style)[$read_only]($read_only_style) "
|
||||
style = "bright-cyan"
|
||||
|
||||
[git_branch]
|
||||
format = "[❯ $symbol$branch(:$remote_branch)]($style) "
|
||||
style = "bright-purple"
|
||||
|
||||
[git_status]
|
||||
style = "bright-purple"
|
||||
|
||||
[rust]
|
||||
format = "[❯ $symbol($version )]($style)"
|
||||
symbol = "rustc "
|
||||
style = "bright-red"
|
||||
|
||||
[nodejs]
|
||||
format = "[❯ $symbol($version )]($style)"
|
||||
symbol = "nodejs "
|
||||
style = "bright-red"
|
||||
|
||||
[memory_usage]
|
||||
format = "[❯ mem ${ram} ${ram_pct}]($style) "
|
||||
threshold = -1
|
||||
style = "white"
|
||||
|
||||
[cmd_duration]
|
||||
format = "[❯ $duration]($style)"
|
||||
style = "bright-yellow"
|
||||
|
||||
[container]
|
||||
format = "[❯ 🦎 core container ]($style)"
|
||||
style = "bright-green"
|
||||
|
||||
[aws]
|
||||
disabled = true
|
||||
@@ -33,9 +33,11 @@ resolver_api.workspace = true
|
||||
run_command.workspace = true
|
||||
svi.workspace = true
|
||||
# external
|
||||
portable-pty.workspace = true
|
||||
axum-server.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_yaml.workspace = true
|
||||
tokio-util.workspace = true
|
||||
futures.workspace = true
|
||||
tracing.workspace = true
|
||||
bollard.workspace = true
|
||||
@@ -45,7 +47,9 @@ anyhow.workspace = true
|
||||
rustls.workspace = true
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
bytes.workspace = true
|
||||
axum.workspace = true
|
||||
clap.workspace = true
|
||||
envy.workspace = true
|
||||
uuid.workspace = true
|
||||
rand.workspace = true
|
||||
|
||||
@@ -15,6 +15,7 @@ RUN cargo build -p komodo_periphery --release
|
||||
# Final Image
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
COPY ./bin/periphery/starship.toml /config/starship.toml
|
||||
COPY ./bin/periphery/debian-deps.sh .
|
||||
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
## Periphery deps installer
|
||||
|
||||
apt-get update
|
||||
apt-get install -y git curl wget ca-certificates
|
||||
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
|
||||
chmod a+r /etc/apt/keyrings/docker.asc
|
||||
@@ -15,4 +19,10 @@ apt-get update
|
||||
# apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
apt-get install -y docker-ce-cli docker-buildx-plugin docker-compose-plugin
|
||||
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Starship prompt
|
||||
curl -sS https://starship.rs/install.sh | sh -s -- --yes --bin-dir /usr/local/bin
|
||||
echo 'export STARSHIP_CONFIG=/config/starship.toml' >> /root/.bashrc
|
||||
echo 'eval "$(starship init bash)"' >> /root/.bashrc
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ FROM ${AARCH64_BINARIES} AS aarch64
|
||||
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
COPY ./bin/periphery/starship.toml /config/starship.toml
|
||||
COPY ./bin/periphery/debian-deps.sh .
|
||||
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ FROM ${BINARIES_IMAGE} AS binaries
|
||||
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
COPY ./bin/periphery/starship.toml /config/starship.toml
|
||||
COPY ./bin/periphery/debian-deps.sh .
|
||||
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=binaries /periphery /usr/local/bin/periphery
|
||||
|
||||
EXPOSE 8120
|
||||
|
||||
@@ -8,11 +8,8 @@ use komodo_client::entities::{
|
||||
update::Log,
|
||||
};
|
||||
use periphery_client::api::{
|
||||
GetDockerLists, GetDockerListsResponse, GetHealth,
|
||||
GetHealthResponse, GetVersion, GetVersionResponse,
|
||||
ListDockerRegistries, ListGitProviders, ListSecrets, PruneSystem,
|
||||
RunCommand, build::*, compose::*, container::*, git::*, image::*,
|
||||
network::*, stats::*, volume::*,
|
||||
build::*, compose::*, container::*, git::*, image::*, network::*,
|
||||
stats::*, terminal::*, volume::*, *,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use response::Response;
|
||||
@@ -27,9 +24,13 @@ mod deploy;
|
||||
mod git;
|
||||
mod image;
|
||||
mod network;
|
||||
mod router;
|
||||
mod stats;
|
||||
mod terminal;
|
||||
mod volume;
|
||||
|
||||
pub use router::router;
|
||||
|
||||
pub struct Args;
|
||||
|
||||
#[derive(
|
||||
@@ -136,6 +137,13 @@ pub enum PeripheryRequest {
|
||||
|
||||
// All in one (Write)
|
||||
PruneSystem(PruneSystem),
|
||||
|
||||
// Terminal
|
||||
ListTerminals(ListTerminals),
|
||||
CreateTerminal(CreateTerminal),
|
||||
DeleteTerminal(DeleteTerminal),
|
||||
DeleteAllTerminals(DeleteAllTerminals),
|
||||
CreateTerminalAuthToken(CreateTerminalAuthToken),
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -8,7 +8,7 @@ use axum::{
|
||||
http::{Request, StatusCode},
|
||||
middleware::{self, Next},
|
||||
response::Response,
|
||||
routing::post,
|
||||
routing::{get, post},
|
||||
};
|
||||
use derive_variants::ExtractVariant;
|
||||
use resolver_api::Resolve;
|
||||
@@ -19,9 +19,13 @@ use crate::config::periphery_config;
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/", post(handler))
|
||||
.merge(
|
||||
Router::new()
|
||||
.route("/", post(handler))
|
||||
.layer(middleware::from_fn(guard_request_by_passkey)),
|
||||
)
|
||||
.route("/terminal", get(super::terminal::connect_terminal))
|
||||
.layer(middleware::from_fn(guard_request_by_ip))
|
||||
.layer(middleware::from_fn(guard_request_by_passkey))
|
||||
}
|
||||
|
||||
async fn handler(
|
||||
333
bin/periphery/src/api/terminal.rs
Normal file
333
bin/periphery/src/api/terminal.rs
Normal file
@@ -0,0 +1,333 @@
|
||||
use std::{collections::HashMap, sync::OnceLock};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
extract::{
|
||||
Query, WebSocketUpgrade,
|
||||
ws::{Message, Utf8Bytes},
|
||||
},
|
||||
http::StatusCode,
|
||||
response::Response,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use komodo_client::entities::{
|
||||
NoData, komodo_timestamp, server::TerminalInfo,
|
||||
};
|
||||
use periphery_client::api::terminal::{
|
||||
ConnectTerminalQuery, CreateTerminal, CreateTerminalAuthToken,
|
||||
CreateTerminalAuthTokenResponse, DeleteAllTerminals,
|
||||
DeleteTerminal, ListTerminals,
|
||||
};
|
||||
use rand::Rng;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{
|
||||
config::periphery_config,
|
||||
terminal::{
|
||||
ResizeDimensions, StdinMsg, clean_up_terminals, create_terminal,
|
||||
delete_all_terminals, delete_terminal, get_terminal,
|
||||
list_terminals,
|
||||
},
|
||||
};
|
||||
|
||||
impl Resolve<super::Args> for ListTerminals {
|
||||
#[instrument(name = "ListTerminals", level = "debug")]
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
) -> serror::Result<Vec<TerminalInfo>> {
|
||||
if periphery_config().disable_terminals {
|
||||
return Err(
|
||||
anyhow!("Terminals are disabled in the periphery config")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
clean_up_terminals().await;
|
||||
Ok(list_terminals().await)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<super::Args> for CreateTerminal {
|
||||
#[instrument(name = "CreateTerminal", level = "debug")]
|
||||
async fn resolve(self, _: &super::Args) -> serror::Result<NoData> {
|
||||
if periphery_config().disable_terminals {
|
||||
return Err(
|
||||
anyhow!("Terminals are disabled in the periphery config")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
create_terminal(self.name, self.command, self.recreate)
|
||||
.await
|
||||
.map(|_| NoData {})
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<super::Args> for DeleteTerminal {
|
||||
#[instrument(name = "DeleteTerminal", level = "debug")]
|
||||
async fn resolve(self, _: &super::Args) -> serror::Result<NoData> {
|
||||
if periphery_config().disable_terminals {
|
||||
return Err(
|
||||
anyhow!("Terminals are disabled in the periphery config")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
delete_terminal(&self.terminal).await;
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<super::Args> for DeleteAllTerminals {
|
||||
#[instrument(name = "DeleteAllTerminals", level = "debug")]
|
||||
async fn resolve(self, _: &super::Args) -> serror::Result<NoData> {
|
||||
if periphery_config().disable_terminals {
|
||||
return Err(
|
||||
anyhow!("Terminals are disabled in the periphery config")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
delete_all_terminals().await;
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<super::Args> for CreateTerminalAuthToken {
|
||||
#[instrument(name = "CreateTerminalAuthToken", level = "debug")]
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
) -> serror::Result<CreateTerminalAuthTokenResponse> {
|
||||
if periphery_config().disable_terminals {
|
||||
return Err(
|
||||
anyhow!("Terminals are disabled in the periphery config")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
Ok(CreateTerminalAuthTokenResponse {
|
||||
token: auth_tokens().create_auth_token(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Tokens valid for 3 seconds
|
||||
const TOKEN_VALID_FOR_MS: i64 = 3_000;
|
||||
|
||||
fn auth_tokens() -> &'static AuthTokens {
|
||||
static AUTH_TOKENS: OnceLock<AuthTokens> = OnceLock::new();
|
||||
AUTH_TOKENS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct AuthTokens {
|
||||
map: std::sync::Mutex<HashMap<String, i64>>,
|
||||
}
|
||||
|
||||
impl AuthTokens {
|
||||
pub fn create_auth_token(&self) -> String {
|
||||
let token: String = rand::rng()
|
||||
.sample_iter(&rand::distr::Alphanumeric)
|
||||
.take(30)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
self
|
||||
.map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(token.clone(), komodo_timestamp() + TOKEN_VALID_FOR_MS);
|
||||
token
|
||||
}
|
||||
|
||||
pub fn check_token(&self, token: String) -> serror::Result<()> {
|
||||
let Some(valid_until) = self.map.lock().unwrap().remove(&token)
|
||||
else {
|
||||
return Err(
|
||||
anyhow!("Terminal auth token not found")
|
||||
.status_code(StatusCode::UNAUTHORIZED),
|
||||
);
|
||||
};
|
||||
if komodo_timestamp() <= valid_until {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(
|
||||
anyhow!("Terminal token is expired")
|
||||
.status_code(StatusCode::UNAUTHORIZED),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect_terminal(
|
||||
Query(ConnectTerminalQuery {
|
||||
token,
|
||||
terminal,
|
||||
init,
|
||||
}): Query<ConnectTerminalQuery>,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> serror::Result<Response> {
|
||||
if periphery_config().disable_terminals {
|
||||
return Err(
|
||||
anyhow!("Terminals are disabled in the periphery config")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
// Auth the connection with single use token
|
||||
auth_tokens().check_token(token)?;
|
||||
|
||||
clean_up_terminals().await;
|
||||
let terminal = get_terminal(&terminal).await?;
|
||||
|
||||
Ok(ws.on_upgrade(|mut socket| async move {
|
||||
let init_res = async {
|
||||
let (a, b) = terminal.history.bytes_parts();
|
||||
if !a.is_empty() {
|
||||
socket.send(Message::Binary(a)).await.context("Failed to send history part a")?;
|
||||
}
|
||||
if !b.is_empty() {
|
||||
socket.send(Message::Binary(b)).await.context("Failed to send history part b")?;
|
||||
}
|
||||
|
||||
if let Some(init) = init {
|
||||
terminal
|
||||
.stdin
|
||||
.send(StdinMsg::Bytes(Bytes::from(init + "\n")))
|
||||
.await
|
||||
.context("Failed to run init command")?
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}.await;
|
||||
|
||||
if let Err(e) = init_res {
|
||||
let _ = socket.send(Message::Text(format!("ERROR: {e:#}").into())).await;
|
||||
let _ = socket.close().await;
|
||||
return;
|
||||
}
|
||||
|
||||
let (mut ws_write, mut ws_read) = socket.split();
|
||||
|
||||
let cancel = CancellationToken::new();
|
||||
|
||||
let ws_read = async {
|
||||
loop {
|
||||
let res = tokio::select! {
|
||||
res = ws_read.next() => res,
|
||||
_ = terminal.cancel.cancelled() => {
|
||||
trace!("ws read: cancelled from outside");
|
||||
break
|
||||
},
|
||||
_ = cancel.cancelled() => {
|
||||
trace!("ws read: cancelled from inside");
|
||||
break;
|
||||
}
|
||||
};
|
||||
match res {
|
||||
Some(Ok(Message::Binary(bytes)))
|
||||
if bytes.first() == Some(&0x00) =>
|
||||
{
|
||||
// println!("Got ws read bytes - for stdin");
|
||||
if let Err(e) = terminal.stdin.send(StdinMsg::Bytes(
|
||||
Bytes::copy_from_slice(&bytes[1..]),
|
||||
)).await {
|
||||
debug!("WS -> PTY channel send error: {e:}");
|
||||
terminal.cancel();
|
||||
break;
|
||||
};
|
||||
}
|
||||
Some(Ok(Message::Binary(bytes)))
|
||||
if bytes.first() == Some(&0xFF) =>
|
||||
{
|
||||
// println!("Got ws read bytes - for resize");
|
||||
if let Ok(dimensions) =
|
||||
serde_json::from_slice::<ResizeDimensions>(&bytes[1..])
|
||||
{
|
||||
if let Err(e) =
|
||||
terminal.stdin.send(StdinMsg::Resize(dimensions)).await
|
||||
{
|
||||
debug!("WS -> PTY channel send error: {e:}");
|
||||
terminal.cancel();
|
||||
break;
|
||||
};
|
||||
}
|
||||
}
|
||||
Some(Ok(Message::Text(text))) => {
|
||||
trace!("Got ws read text");
|
||||
if let Err(e) =
|
||||
terminal.stdin.send(StdinMsg::Bytes(Bytes::from(text))).await
|
||||
{
|
||||
debug!("WS -> PTY channel send error: {e:?}");
|
||||
terminal.cancel();
|
||||
break;
|
||||
};
|
||||
}
|
||||
Some(Ok(Message::Close(_))) => {
|
||||
debug!("got ws read close");
|
||||
cancel.cancel();
|
||||
break;
|
||||
}
|
||||
Some(Ok(_)) => {
|
||||
// Do nothing (ping, non-prefixed bytes, etc.)
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
debug!("Got ws read error: {e:?}");
|
||||
cancel.cancel();
|
||||
break;
|
||||
}
|
||||
None => {
|
||||
debug!("Got ws read none");
|
||||
cancel.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let ws_write = async {
|
||||
let mut stdout = terminal.stdout.resubscribe();
|
||||
loop {
|
||||
let res = tokio::select! {
|
||||
res = stdout.recv() => res.context("Failed to get message over stdout receiver"),
|
||||
_ = terminal.cancel.cancelled() => {
|
||||
trace!("ws write: cancelled from outside");
|
||||
let _ = ws_write.send(Message::Text(Utf8Bytes::from_static("PTY KILLED"))).await;
|
||||
if let Err(e) = ws_write.close().await {
|
||||
debug!("Failed to close ws: {e:?}");
|
||||
};
|
||||
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:?}");
|
||||
};
|
||||
break
|
||||
}
|
||||
};
|
||||
match res {
|
||||
Ok(bytes) => {
|
||||
if let Err(e) =
|
||||
ws_write.send(Message::Binary(bytes)).await
|
||||
{
|
||||
debug!("Failed to send to WS: {e:?}");
|
||||
cancel.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("PTY -> WS channel read error: {e:?}");
|
||||
let _ = ws_write.send(Message::Text(Utf8Bytes::from(format!("ERROR: {e:#}")))).await;
|
||||
let _ = ws_write.close().await;
|
||||
terminal.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tokio::join!(ws_read, ws_write);
|
||||
|
||||
clean_up_terminals().await;
|
||||
}))
|
||||
}
|
||||
@@ -42,6 +42,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),
|
||||
disable_terminals: env
|
||||
.periphery_disable_terminals
|
||||
.unwrap_or(config.disable_terminals),
|
||||
stats_polling_rate: env
|
||||
.periphery_stats_polling_rate
|
||||
.unwrap_or(config.stats_polling_rate),
|
||||
|
||||
@@ -12,9 +12,9 @@ mod compose;
|
||||
mod config;
|
||||
mod docker;
|
||||
mod helpers;
|
||||
mod router;
|
||||
mod ssl;
|
||||
mod stats;
|
||||
mod terminal;
|
||||
|
||||
async fn app() -> anyhow::Result<()> {
|
||||
dotenvy::dotenv().ok();
|
||||
@@ -35,8 +35,8 @@ async fn app() -> anyhow::Result<()> {
|
||||
let socket_addr = SocketAddr::from_str(&addr)
|
||||
.context("failed to parse listen address")?;
|
||||
|
||||
let app = router::router()
|
||||
.into_make_service_with_connect_info::<SocketAddr>();
|
||||
let app =
|
||||
api::router().into_make_service_with_connect_info::<SocketAddr>();
|
||||
|
||||
if config.ssl_enabled {
|
||||
info!("🔒 Periphery SSL Enabled");
|
||||
@@ -73,7 +73,10 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
tokio::select! {
|
||||
res = app => return res?,
|
||||
_ = term_signal.recv() => {},
|
||||
_ = term_signal.recv() => {
|
||||
info!("Exiting all active Terminals for shutdown");
|
||||
terminal::delete_all_terminals().await;
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -201,5 +201,6 @@ fn get_system_information(
|
||||
.next()
|
||||
.map(|cpu| cpu.brand().to_string())
|
||||
.unwrap_or_default(),
|
||||
terminals_disabled: periphery_config().disable_terminals,
|
||||
}
|
||||
}
|
||||
|
||||
346
bin/periphery/src/terminal.rs
Normal file
346
bin/periphery/src/terminal.rs
Normal file
@@ -0,0 +1,346 @@
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
sync::{Arc, OnceLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use bytes::Bytes;
|
||||
use komodo_client::{
|
||||
api::write::TerminalRecreateMode, entities::server::TerminalInfo,
|
||||
};
|
||||
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
type PtyName = String;
|
||||
type PtyMap = tokio::sync::RwLock<HashMap<PtyName, Arc<Terminal>>>;
|
||||
type StdinSender = mpsc::Sender<StdinMsg>;
|
||||
type StdoutReceiver = broadcast::Receiver<Bytes>;
|
||||
|
||||
pub async fn create_terminal(
|
||||
name: String,
|
||||
command: String,
|
||||
recreate: TerminalRecreateMode,
|
||||
) -> anyhow::Result<()> {
|
||||
trace!(
|
||||
"CreateTerminal: {name} | command: {command} | recreate: {recreate:?}"
|
||||
);
|
||||
let mut terminals = terminals().write().await;
|
||||
use TerminalRecreateMode::*;
|
||||
if matches!(recreate, Never | DifferentCommand) {
|
||||
if let Some(terminal) = terminals.get(&name) {
|
||||
if terminal.command == command {
|
||||
return Ok(());
|
||||
} else if matches!(recreate, Never) {
|
||||
return Err(anyhow!(
|
||||
"Terminal {name} already exists, but has command {} instead of {command}",
|
||||
terminal.command
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(prev) = terminals.insert(
|
||||
name,
|
||||
Terminal::new(command)
|
||||
.await
|
||||
.context("Failed to init terminal")?
|
||||
.into(),
|
||||
) {
|
||||
prev.cancel();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_terminal(name: &str) {
|
||||
if let Some(terminal) = terminals().write().await.remove(name) {
|
||||
terminal.cancel.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_terminals() -> Vec<TerminalInfo> {
|
||||
let mut terminals = terminals()
|
||||
.read()
|
||||
.await
|
||||
.iter()
|
||||
.map(|(name, terminal)| TerminalInfo {
|
||||
name: name.to_string(),
|
||||
command: terminal.command.clone(),
|
||||
stored_size_kb: terminal.history.size_kb(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
terminals.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
terminals
|
||||
}
|
||||
|
||||
pub async fn get_terminal(
|
||||
name: &str,
|
||||
) -> anyhow::Result<Arc<Terminal>> {
|
||||
terminals()
|
||||
.read()
|
||||
.await
|
||||
.get(name)
|
||||
.cloned()
|
||||
.with_context(|| format!("No terminal at {name}"))
|
||||
}
|
||||
|
||||
pub async fn clean_up_terminals() {
|
||||
terminals()
|
||||
.write()
|
||||
.await
|
||||
.retain(|_, terminal| !terminal.cancel.is_cancelled());
|
||||
}
|
||||
|
||||
pub async fn delete_all_terminals() {
|
||||
terminals()
|
||||
.write()
|
||||
.await
|
||||
.drain()
|
||||
.for_each(|(_, terminal)| terminal.cancel());
|
||||
// The terminals poll cancel every 500 millis, need to wait for them
|
||||
// to finish cancelling.
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
fn terminals() -> &'static PtyMap {
|
||||
static TERMINALS: OnceLock<PtyMap> = OnceLock::new();
|
||||
TERMINALS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Deserialize)]
|
||||
pub struct ResizeDimensions {
|
||||
rows: u16,
|
||||
cols: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum StdinMsg {
|
||||
Bytes(Bytes),
|
||||
Resize(ResizeDimensions),
|
||||
}
|
||||
|
||||
pub struct Terminal {
|
||||
/// The command that was used as the root command, eg `shell`
|
||||
command: String,
|
||||
|
||||
pub cancel: CancellationToken,
|
||||
|
||||
pub stdin: StdinSender,
|
||||
pub stdout: StdoutReceiver,
|
||||
|
||||
pub history: Arc<History>,
|
||||
}
|
||||
|
||||
impl Terminal {
|
||||
async fn new(command: String) -> anyhow::Result<Terminal> {
|
||||
trace!("Creating terminal with command: {command}");
|
||||
|
||||
let terminal = native_pty_system()
|
||||
.openpty(PtySize::default())
|
||||
.context("Failed to open terminal")?;
|
||||
|
||||
let mut command_split = command.split(' ').map(|arg| arg.trim());
|
||||
let cmd =
|
||||
command_split.next().context("Command cannot be empty")?;
|
||||
|
||||
let mut cmd = CommandBuilder::new(cmd);
|
||||
|
||||
for arg in command_split {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
|
||||
cmd.env("TERM", "xterm-256color");
|
||||
cmd.env("COLORTERM", "truecolor");
|
||||
|
||||
let mut child = terminal
|
||||
.slave
|
||||
.spawn_command(cmd)
|
||||
.context("Failed to spawn child command")?;
|
||||
|
||||
// Check the child didn't stop immediately (after a little wait) with error
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
if let Some(status) = child
|
||||
.try_wait()
|
||||
.context("Failed to check child process exit status")?
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"Child process exited immediately with code {}",
|
||||
status.exit_code()
|
||||
));
|
||||
}
|
||||
|
||||
let mut terminal_write = terminal
|
||||
.master
|
||||
.take_writer()
|
||||
.context("Failed to take terminal writer")?;
|
||||
let mut terminal_read = terminal
|
||||
.master
|
||||
.try_clone_reader()
|
||||
.context("Failed to clone terminal reader")?;
|
||||
|
||||
let cancel = CancellationToken::new();
|
||||
|
||||
// CHILD WAIT TASK
|
||||
let _cancel = cancel.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
loop {
|
||||
if _cancel.is_cancelled() {
|
||||
trace!("child wait handle cancelled from outside");
|
||||
if let Err(e) = child.kill() {
|
||||
debug!("Failed to kill child | {e:?}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
match child.try_wait() {
|
||||
Ok(Some(code)) => {
|
||||
debug!("child exited with code {code}");
|
||||
_cancel.cancel();
|
||||
break;
|
||||
}
|
||||
Ok(None) => {
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("failed to wait for child | {e:?}");
|
||||
_cancel.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// WS (channel) -> STDIN TASK
|
||||
// Theres only one consumer here, so use mpsc
|
||||
let (stdin, mut channel_read) =
|
||||
tokio::sync::mpsc::channel::<StdinMsg>(8192);
|
||||
let _cancel = cancel.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
loop {
|
||||
if _cancel.is_cancelled() {
|
||||
trace!("terminal write: cancelled from outside");
|
||||
break;
|
||||
}
|
||||
match channel_read.blocking_recv() {
|
||||
Some(StdinMsg::Bytes(bytes)) => {
|
||||
if let Err(e) = terminal_write.write_all(&bytes) {
|
||||
debug!("Failed to write to PTY: {e:?}");
|
||||
_cancel.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(StdinMsg::Resize(dimensions)) => {
|
||||
if let Err(e) = terminal.master.resize(PtySize {
|
||||
cols: dimensions.cols,
|
||||
rows: dimensions.rows,
|
||||
pixel_width: 0,
|
||||
pixel_height: 0,
|
||||
}) {
|
||||
debug!("Failed to resize | {e:?}");
|
||||
_cancel.cancel();
|
||||
break;
|
||||
};
|
||||
}
|
||||
None => {
|
||||
debug!("WS -> PTY channel read error: Disconnected");
|
||||
_cancel.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let history = Arc::new(History::default());
|
||||
|
||||
// PTY -> WS (channel) TASK
|
||||
// Uses broadcast to output to multiple client simultaneously
|
||||
let (write, stdout) =
|
||||
tokio::sync::broadcast::channel::<Bytes>(8192);
|
||||
let _cancel = cancel.clone();
|
||||
let _history = history.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut buf = [0u8; 8192];
|
||||
loop {
|
||||
if _cancel.is_cancelled() {
|
||||
trace!("terminal read: cancelled from outside");
|
||||
break;
|
||||
}
|
||||
match terminal_read.read(&mut buf) {
|
||||
Ok(0) => {
|
||||
// EOF
|
||||
trace!("Got PTY read EOF");
|
||||
_cancel.cancel();
|
||||
break;
|
||||
}
|
||||
Ok(n) => {
|
||||
_history.push(&buf[..n]);
|
||||
if let Err(e) =
|
||||
write.send(Bytes::copy_from_slice(&buf[..n]))
|
||||
{
|
||||
debug!("PTY -> WS channel send error: {e:?}");
|
||||
_cancel.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Failed to read for PTY: {e:?}");
|
||||
_cancel.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
trace!("terminal tasks spawned");
|
||||
|
||||
Ok(Terminal {
|
||||
command,
|
||||
cancel,
|
||||
stdin,
|
||||
stdout,
|
||||
history,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cancel(&self) {
|
||||
trace!("Cancel called");
|
||||
self.cancel.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/// 1 MiB max history size per terminal
|
||||
const MAX_BYTES: usize = 1024 * 1024;
|
||||
|
||||
pub struct History {
|
||||
buf: std::sync::RwLock<VecDeque<u8>>,
|
||||
}
|
||||
|
||||
impl Default for History {
|
||||
fn default() -> Self {
|
||||
History {
|
||||
buf: VecDeque::with_capacity(MAX_BYTES).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl History {
|
||||
/// Push some bytes, evicting the oldest when full.
|
||||
fn push(&self, bytes: &[u8]) {
|
||||
let mut buf = self.buf.write().unwrap();
|
||||
for byte in bytes {
|
||||
if buf.len() == MAX_BYTES {
|
||||
buf.pop_front();
|
||||
}
|
||||
buf.push_back(*byte);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bytes_parts(&self) -> (Bytes, Bytes) {
|
||||
let buf = self.buf.read().unwrap();
|
||||
let (a, b) = buf.as_slices();
|
||||
(Bytes::copy_from_slice(a), Bytes::copy_from_slice(b))
|
||||
}
|
||||
|
||||
pub fn size_kb(&self) -> f64 {
|
||||
self.buf.read().unwrap().len() as f64 / 1024.0
|
||||
}
|
||||
}
|
||||
67
bin/periphery/starship.toml
Normal file
67
bin/periphery/starship.toml
Normal file
@@ -0,0 +1,67 @@
|
||||
## This is used to customize the shell prompt in Periphery container for Terminals
|
||||
|
||||
"$schema" = 'https://starship.rs/config-schema.json'
|
||||
|
||||
add_newline = true
|
||||
|
||||
format = "$time$hostname$container$memory_usage$all"
|
||||
|
||||
[character]
|
||||
success_symbol = "[❯](bright-blue bold)"
|
||||
error_symbol = "[❯](bright-red bold)"
|
||||
|
||||
[package]
|
||||
disabled = true
|
||||
|
||||
[time]
|
||||
format = "[❯$time](white dimmed) "
|
||||
time_format = "%l:%M %p"
|
||||
utc_time_offset = '-5'
|
||||
disabled = true
|
||||
|
||||
[username]
|
||||
format = "[❯ $user]($style) "
|
||||
style_user = "bright-green"
|
||||
show_always = true
|
||||
|
||||
[hostname]
|
||||
format = "[❯ $hostname]($style) "
|
||||
style = "bright-blue"
|
||||
ssh_only = false
|
||||
|
||||
[directory]
|
||||
format = "[❯ $path]($style)[$read_only]($read_only_style) "
|
||||
style = "bright-cyan"
|
||||
|
||||
[git_branch]
|
||||
format = "[❯ $symbol$branch(:$remote_branch)]($style) "
|
||||
style = "bright-purple"
|
||||
|
||||
[git_status]
|
||||
style = "bright-purple"
|
||||
|
||||
[rust]
|
||||
format = "[❯ $symbol($version )]($style)"
|
||||
symbol = "rustc "
|
||||
style = "bright-red"
|
||||
|
||||
[nodejs]
|
||||
format = "[❯ $symbol($version )]($style)"
|
||||
symbol = "nodejs "
|
||||
style = "bright-red"
|
||||
|
||||
[memory_usage]
|
||||
format = "[❯ mem ${ram} ${ram_pct}]($style) "
|
||||
threshold = -1
|
||||
style = "white"
|
||||
|
||||
[cmd_duration]
|
||||
format = "[❯ $duration]($style)"
|
||||
style = "bright-yellow"
|
||||
|
||||
[container]
|
||||
format = "[❯ 🦎 periphery container ]($style)"
|
||||
style = "bright-green"
|
||||
|
||||
[aws]
|
||||
disabled = true
|
||||
@@ -68,6 +68,7 @@
|
||||
|
||||
pub mod auth;
|
||||
pub mod execute;
|
||||
pub mod terminal;
|
||||
pub mod read;
|
||||
pub mod user;
|
||||
pub mod write;
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::entities::{
|
||||
},
|
||||
server::{
|
||||
Server, ServerActionState, ServerListItem, ServerQuery,
|
||||
ServerState,
|
||||
ServerState, TerminalInfo,
|
||||
},
|
||||
stack::ComposeProject,
|
||||
stats::{
|
||||
@@ -628,3 +628,27 @@ pub struct GetServersSummaryResponse {
|
||||
/// The number of disabled servers.
|
||||
pub disabled: I64,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/// List the current terminals on specified server.
|
||||
/// Response: [ListTerminalsResponse].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoReadRequest)]
|
||||
#[response(ListTerminalsResponse)]
|
||||
#[error(serror::Error)]
|
||||
pub struct ListTerminals {
|
||||
/// Id or name
|
||||
#[serde(alias = "id", alias = "name")]
|
||||
pub server: String,
|
||||
/// 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 ListTerminalsResponse = Vec<TerminalInfo>;
|
||||
|
||||
20
client/core/rs/src/api/terminal.rs
Normal file
20
client/core/rs/src/api/terminal.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
/// Query to connect to a terminal (interactive shell over websocket) on the given server.
|
||||
/// TODO: Document calling.
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ConnectTerminalQuery {
|
||||
/// Server Id or name
|
||||
pub server: String,
|
||||
/// Each periphery can keep multiple terminals open.
|
||||
/// If a terminals with the specified name already exists,
|
||||
/// it will be attached to.
|
||||
/// Otherwise a new terminal will be created for the command,
|
||||
/// which will persist until it is deleted using
|
||||
/// [DeleteTerminal][crate::api::write::server::DeleteTerminal]
|
||||
pub terminal: String,
|
||||
/// Optional. The initial command to execute on connection to the shell.
|
||||
pub init: Option<String>,
|
||||
}
|
||||
@@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::entities::{
|
||||
NoData,
|
||||
server::{_PartialServerConfig, Server},
|
||||
update::Update,
|
||||
};
|
||||
@@ -105,3 +106,83 @@ pub struct CreateNetwork {
|
||||
/// The name of the network to create.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/// Configures the behavior of [CreateTerminal] if the
|
||||
/// specified terminal name already exists.
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
|
||||
pub enum TerminalRecreateMode {
|
||||
/// Never kill the old terminal if it already exists.
|
||||
/// If the command is different, returns error.
|
||||
#[default]
|
||||
Never,
|
||||
/// Always kill the old terminal and create new one
|
||||
Always,
|
||||
/// Only kill and recreate if the command is different.
|
||||
DifferentCommand,
|
||||
}
|
||||
|
||||
/// Create a terminal on the server.
|
||||
/// Response: [NoData]
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(NoData)]
|
||||
#[error(serror::Error)]
|
||||
pub struct CreateTerminal {
|
||||
/// Server Id or name
|
||||
pub server: String,
|
||||
/// The name of the terminal on the server to create.
|
||||
pub name: String,
|
||||
/// The shell command (eg `bash`) to init the shell.
|
||||
///
|
||||
/// This can also include args:
|
||||
/// `docker exec -it container sh`
|
||||
///
|
||||
/// Default: `bash`
|
||||
#[serde(default = "default_command")]
|
||||
pub command: String,
|
||||
/// Default: `Never`
|
||||
#[serde(default)]
|
||||
pub recreate: TerminalRecreateMode,
|
||||
}
|
||||
|
||||
fn default_command() -> String {
|
||||
String::from("bash")
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/// Delete a terminal on the server.
|
||||
/// Response: [NoData]
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(NoData)]
|
||||
#[error(serror::Error)]
|
||||
pub struct DeleteTerminal {
|
||||
/// Server Id or name
|
||||
pub server: String,
|
||||
/// The name of the terminal on the server to delete.
|
||||
pub terminal: String,
|
||||
}
|
||||
|
||||
/// Delete all terminals on the server.
|
||||
/// Response: [NoData]
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(NoData)]
|
||||
#[error(serror::Error)]
|
||||
pub struct DeleteAllTerminals {
|
||||
/// Server Id or name
|
||||
pub server: String,
|
||||
}
|
||||
|
||||
@@ -124,6 +124,8 @@ pub struct Env {
|
||||
pub periphery_stack_dir: Option<PathBuf>,
|
||||
/// Override `build_dir`
|
||||
pub periphery_build_dir: Option<PathBuf>,
|
||||
/// Override `disable_terminals`
|
||||
pub periphery_disable_terminals: Option<bool>,
|
||||
/// Override `stats_polling_rate`
|
||||
pub periphery_stats_polling_rate: Option<Timelength>,
|
||||
/// Override `legacy_compose_cli`
|
||||
@@ -203,6 +205,12 @@ pub struct PeripheryConfig {
|
||||
/// Default: empty
|
||||
pub build_dir: Option<PathBuf>,
|
||||
|
||||
/// Whether to disable the terminal APIs
|
||||
/// and disallow remote shell access.
|
||||
/// Default: false
|
||||
#[serde(default)]
|
||||
pub disable_terminals: bool,
|
||||
|
||||
/// The rate at which the system stats will be polled to update the cache.
|
||||
/// Default: `5-sec`
|
||||
#[serde(default = "default_stats_polling_rate")]
|
||||
@@ -298,6 +306,7 @@ impl Default for PeripheryConfig {
|
||||
repo_dir: None,
|
||||
stack_dir: None,
|
||||
build_dir: None,
|
||||
disable_terminals: Default::default(),
|
||||
stats_polling_rate: default_stats_polling_rate(),
|
||||
legacy_compose_cli: Default::default(),
|
||||
logging: Default::default(),
|
||||
@@ -324,6 +333,7 @@ impl PeripheryConfig {
|
||||
repo_dir: self.repo_dir.clone(),
|
||||
stack_dir: self.stack_dir.clone(),
|
||||
build_dir: self.build_dir.clone(),
|
||||
disable_terminals: self.disable_terminals,
|
||||
stats_polling_rate: self.stats_polling_rate,
|
||||
legacy_compose_cli: self.legacy_compose_cli,
|
||||
logging: self.logging.clone(),
|
||||
|
||||
@@ -1009,3 +1009,5 @@ pub enum ScheduleFormat {
|
||||
English,
|
||||
Cron,
|
||||
}
|
||||
|
||||
pub const KOMODO_EXIT_DATA: &str = "__KOMODO_EXIT_DATA:";
|
||||
|
||||
@@ -38,6 +38,8 @@ pub struct ServerListItemInfo {
|
||||
pub send_mem_alerts: bool,
|
||||
/// Whether server is configured to send disk alerts.
|
||||
pub send_disk_alerts: bool,
|
||||
/// Whether terminals are disabled for this Server.
|
||||
pub terminals_disabled: bool,
|
||||
}
|
||||
|
||||
#[typeshare(serialized_as = "Partial<ServerConfig>")]
|
||||
@@ -276,6 +278,19 @@ pub struct ServerHealth {
|
||||
pub disks: HashMap<PathBuf, ServerHealthState>,
|
||||
}
|
||||
|
||||
/// Info about an active terminal on a server.
|
||||
/// Retrieve with [ListTerminals][crate::api::read::server::ListTerminals].
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
pub struct TerminalInfo {
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// Current pending actions on the server.
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
|
||||
|
||||
@@ -21,6 +21,8 @@ pub struct SystemInformation {
|
||||
pub host_name: Option<String>,
|
||||
/// The CPU's brand
|
||||
pub cpu_brand: String,
|
||||
/// Whether terminals are disabled on this Periphery
|
||||
pub terminals_disabled: bool,
|
||||
}
|
||||
|
||||
/// System stats stored on the database.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use serde_json::json;
|
||||
use serror::deserialize_error;
|
||||
@@ -208,7 +207,7 @@ impl KomodoClient {
|
||||
let res =
|
||||
req.send().await.context("failed to reach Komodo API")?;
|
||||
let status = res.status();
|
||||
if status == StatusCode::OK {
|
||||
if status.is_success() {
|
||||
match res.json().await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(e) => Err(anyhow!("{e:#?}").context(status)),
|
||||
@@ -236,7 +235,7 @@ impl KomodoClient {
|
||||
.json(&body);
|
||||
let res = req.send().context("failed to reach Komodo API")?;
|
||||
let status = res.status();
|
||||
if status == StatusCode::OK {
|
||||
if status.is_success() {
|
||||
match res.json() {
|
||||
Ok(res) => Ok(res),
|
||||
Err(e) => Err(anyhow!("{e:#?}").context(status)),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "komodo_client",
|
||||
"version": "1.17.3",
|
||||
"version": "1.17.4",
|
||||
"description": "Komodo client package",
|
||||
"homepage": "https://komo.do",
|
||||
"main": "dist/lib.js",
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import {
|
||||
AuthRequest,
|
||||
BatchExecutionResponse,
|
||||
ConnectTerminalQuery,
|
||||
ExecuteRequest,
|
||||
ReadRequest,
|
||||
Update,
|
||||
@@ -42,7 +43,7 @@ export function KomodoClient(url: string, options: InitOptions) {
|
||||
secret: options.type === "api-key" ? options.params.secret : undefined,
|
||||
};
|
||||
|
||||
const request = async <Req, Res>(
|
||||
const request = <Req, Res>(
|
||||
path: "/auth" | "/user" | "/read" | "/execute" | "/write",
|
||||
request: Req
|
||||
): Promise<Res> =>
|
||||
@@ -270,6 +271,62 @@ export function KomodoClient(url: string, options: InitOptions) {
|
||||
}
|
||||
};
|
||||
|
||||
const connect_terminal = ({
|
||||
query,
|
||||
on_message,
|
||||
on_login,
|
||||
on_open,
|
||||
on_close,
|
||||
}: {
|
||||
query: ConnectTerminalQuery;
|
||||
on_message?: (e: MessageEvent<any>) => void;
|
||||
on_login?: () => void;
|
||||
on_open?: () => void;
|
||||
on_close?: () => void;
|
||||
}) => {
|
||||
const url_query = new URLSearchParams(
|
||||
query as any as Record<string, string>
|
||||
).toString();
|
||||
const ws = new WebSocket(
|
||||
url.replace("http", "ws") + "/ws/terminal?" + url_query
|
||||
);
|
||||
// Handle login on websocket open
|
||||
ws.onopen = () => {
|
||||
const login_msg: WsLoginMessage =
|
||||
options.type === "jwt"
|
||||
? {
|
||||
type: "Jwt",
|
||||
params: {
|
||||
jwt: options.params.jwt,
|
||||
},
|
||||
}
|
||||
: {
|
||||
type: "ApiKeys",
|
||||
params: {
|
||||
key: options.params.key,
|
||||
secret: options.params.secret,
|
||||
},
|
||||
};
|
||||
ws.send(JSON.stringify(login_msg));
|
||||
on_open?.();
|
||||
};
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
if (e.data == "LOGGED_IN") {
|
||||
ws.binaryType = "arraybuffer";
|
||||
ws.onmessage = (e) => on_message?.(e);
|
||||
on_login?.();
|
||||
return;
|
||||
} else {
|
||||
on_message?.(e);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => on_close?.();
|
||||
|
||||
return ws;
|
||||
};
|
||||
|
||||
return {
|
||||
/**
|
||||
* Call the `/auth` api.
|
||||
@@ -360,5 +417,10 @@ export function KomodoClient(url: string, options: InitOptions) {
|
||||
* Note. Awaiting this method will never finish.
|
||||
*/
|
||||
subscribe_to_update_websocket,
|
||||
/**
|
||||
* Subscribes to terminal io over websocket message,
|
||||
* for use with xtermjs.
|
||||
*/
|
||||
connect_terminal,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ export type ReadResponses = {
|
||||
GetHistoricalServerStats: Types.GetHistoricalServerStatsResponse;
|
||||
ListServers: Types.ListServersResponse;
|
||||
ListFullServers: Types.ListFullServersResponse;
|
||||
ListTerminals: Types.ListTerminalsResponse;
|
||||
|
||||
// ==== DEPLOYMENT ====
|
||||
GetDeploymentsSummary: Types.GetDeploymentsSummaryResponse;
|
||||
@@ -213,6 +214,9 @@ export type WriteResponses = {
|
||||
UpdateServer: Types.Server;
|
||||
RenameServer: Types.Update;
|
||||
CreateNetwork: Types.Update;
|
||||
CreateTerminal: Types.NoData;
|
||||
DeleteTerminal: Types.NoData;
|
||||
DeleteAllTerminals: Types.NoData;
|
||||
|
||||
// ==== DEPLOYMENT ====
|
||||
CreateDeployment: Types.Deployment;
|
||||
|
||||
@@ -1999,6 +1999,8 @@ export interface SystemInformation {
|
||||
host_name?: string;
|
||||
/** The CPU's brand */
|
||||
cpu_brand: string;
|
||||
/** Whether terminals are disabled on this Periphery */
|
||||
terminals_disabled: boolean;
|
||||
}
|
||||
|
||||
export type GetSystemInformationResponse = SystemInformation;
|
||||
@@ -3546,6 +3548,8 @@ export interface ServerListItemInfo {
|
||||
send_mem_alerts: boolean;
|
||||
/** Whether server is configured to send disk alerts. */
|
||||
send_disk_alerts: boolean;
|
||||
/** Whether terminals are disabled for this Server. */
|
||||
terminals_disabled: boolean;
|
||||
}
|
||||
|
||||
export type ServerListItem = ResourceListItem<ServerListItemInfo>;
|
||||
@@ -3670,6 +3674,21 @@ export type ListSystemProcessesResponse = SystemProcess[];
|
||||
|
||||
export type ListTagsResponse = Tag[];
|
||||
|
||||
/**
|
||||
* Info about an active terminal on a server.
|
||||
* Retrieve with [ListTerminals][crate::api::read::server::ListTerminals].
|
||||
*/
|
||||
export interface TerminalInfo {
|
||||
/** 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;
|
||||
}
|
||||
|
||||
export type ListTerminalsResponse = TerminalInfo[];
|
||||
|
||||
export type ListUserGroupsResponse = UserGroup[];
|
||||
|
||||
export type ListUserTargetPermissionsResponse = Permission[];
|
||||
@@ -4217,6 +4236,26 @@ export interface CommitSync {
|
||||
sync: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query to connect to a terminal (interactive shell over websocket) on the given server.
|
||||
* TODO: Document calling.
|
||||
*/
|
||||
export interface ConnectTerminalQuery {
|
||||
/** Server Id or name */
|
||||
server: string;
|
||||
/**
|
||||
* Each periphery can keep multiple terminals open.
|
||||
* If a terminals with the specified name already exists,
|
||||
* it will be attached to.
|
||||
* Otherwise a new terminal will be created for the command,
|
||||
* which will persist until it is deleted using
|
||||
* [DeleteTerminal][crate::api::write::server::DeleteTerminal]
|
||||
*/
|
||||
terminal: string;
|
||||
/** Optional. The initial command to execute on connection to the shell. */
|
||||
init?: string;
|
||||
}
|
||||
|
||||
export interface Conversion {
|
||||
/** reference on the server. */
|
||||
local: string;
|
||||
@@ -4607,6 +4646,44 @@ export interface CreateTag {
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the behavior of [CreateTerminal] if the
|
||||
* specified terminal name already exists.
|
||||
*/
|
||||
export enum TerminalRecreateMode {
|
||||
/**
|
||||
* Never kill the old terminal if it already exists.
|
||||
* If the command is different, returns error.
|
||||
*/
|
||||
Never = "Never",
|
||||
/** Always kill the old terminal and create new one */
|
||||
Always = "Always",
|
||||
/** Only kill and recreate if the command is different. */
|
||||
DifferentCommand = "DifferentCommand",
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a terminal on the server.
|
||||
* Response: [NoData]
|
||||
*/
|
||||
export interface CreateTerminal {
|
||||
/** Server Id or name */
|
||||
server: string;
|
||||
/** The name of the terminal on the server to create. */
|
||||
name: string;
|
||||
/**
|
||||
* The shell command (eg `bash`) to init the shell.
|
||||
*
|
||||
* This can also include args:
|
||||
* `docker exec -it container sh`
|
||||
*
|
||||
* Default: `bash`
|
||||
*/
|
||||
command: string;
|
||||
/** Default: `Never` */
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
|
||||
/** **Admin only.** Create a user group. Response: [UserGroup] */
|
||||
export interface CreateUserGroup {
|
||||
/** The name to assign to the new UserGroup */
|
||||
@@ -4658,6 +4735,15 @@ export interface DeleteAlerter {
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all terminals on the server.
|
||||
* Response: [NoData]
|
||||
*/
|
||||
export interface DeleteAllTerminals {
|
||||
/** Server Id or name */
|
||||
server: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an api key for the calling user.
|
||||
* Response: [NoData]
|
||||
@@ -4851,6 +4937,17 @@ export interface DeleteTag {
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a terminal on the server.
|
||||
* Response: [NoData]
|
||||
*/
|
||||
export interface DeleteTerminal {
|
||||
/** Server Id or name */
|
||||
server: string;
|
||||
/** The name of the terminal on the server to delete. */
|
||||
terminal: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* **Admin only**. Delete a user.
|
||||
* Admins can delete any non-admin user.
|
||||
@@ -6437,6 +6534,20 @@ export interface ListTags {
|
||||
query?: MongoDocument;
|
||||
}
|
||||
|
||||
/**
|
||||
* List the current terminals on specified server.
|
||||
* Response: [ListTerminalsResponse].
|
||||
*/
|
||||
export interface ListTerminals {
|
||||
/** Id or name */
|
||||
server: string;
|
||||
/**
|
||||
* Force a fresh call to Periphery for the list.
|
||||
* Otherwise the response will be cached for 30s
|
||||
*/
|
||||
fresh?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginated endpoint for updates matching optional query.
|
||||
* More recent updates will be returned first.
|
||||
@@ -7870,6 +7981,7 @@ export type ReadRequest =
|
||||
| { type: "ListDockerImages", params: ListDockerImages }
|
||||
| { type: "ListDockerVolumes", params: ListDockerVolumes }
|
||||
| { type: "ListComposeProjects", params: ListComposeProjects }
|
||||
| { type: "ListTerminals", params: ListTerminals }
|
||||
| { type: "GetDeploymentsSummary", params: GetDeploymentsSummary }
|
||||
| { type: "GetDeployment", params: GetDeployment }
|
||||
| { type: "GetDeploymentContainer", params: GetDeploymentContainer }
|
||||
@@ -7968,6 +8080,9 @@ export type WriteRequest =
|
||||
| { type: "UpdateServer", params: UpdateServer }
|
||||
| { type: "RenameServer", params: RenameServer }
|
||||
| { type: "CreateNetwork", params: CreateNetwork }
|
||||
| { type: "CreateTerminal", params: CreateTerminal }
|
||||
| { type: "DeleteTerminal", params: DeleteTerminal }
|
||||
| { type: "DeleteAllTerminals", params: DeleteAllTerminals }
|
||||
| { type: "CreateDeployment", params: CreateDeployment }
|
||||
| { type: "CopyDeployment", params: CopyDeployment }
|
||||
| { type: "CreateDeploymentFromContainer", params: CreateDeploymentFromContainer }
|
||||
|
||||
@@ -13,11 +13,15 @@ repository.workspace = true
|
||||
# local
|
||||
komodo_client.workspace = true
|
||||
# mogh
|
||||
serror.workspace = true
|
||||
resolver_api.workspace = true
|
||||
serror.workspace = true
|
||||
# external
|
||||
reqwest.workspace = true
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
tokio-tungstenite.workspace = true
|
||||
serde_json.workspace = true
|
||||
tracing.workspace = true
|
||||
serde_qs.workspace = true
|
||||
reqwest.workspace = true
|
||||
tracing.workspace = true
|
||||
anyhow.workspace = true
|
||||
rustls.workspace = true
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
@@ -19,6 +19,7 @@ pub mod git;
|
||||
pub mod image;
|
||||
pub mod network;
|
||||
pub mod stats;
|
||||
pub mod terminal;
|
||||
pub mod volume;
|
||||
|
||||
//
|
||||
|
||||
80
client/periphery/rs/src/api/terminal.rs
Normal file
80
client/periphery/rs/src/api/terminal.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use komodo_client::{
|
||||
api::write::TerminalRecreateMode,
|
||||
entities::{NoData, server::TerminalInfo},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]
|
||||
#[response(Vec<TerminalInfo>)]
|
||||
#[error(serror::Error)]
|
||||
pub struct ListTerminals {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]
|
||||
#[response(NoData)]
|
||||
#[error(serror::Error)]
|
||||
pub struct CreateTerminal {
|
||||
/// The name of the terminal to create
|
||||
pub name: String,
|
||||
/// The shell command (eg `bash`) to init the shell.
|
||||
///
|
||||
/// This can also include args:
|
||||
/// `docker exec -it container sh`
|
||||
#[serde(default = "default_command")]
|
||||
pub command: String,
|
||||
/// Default: `Never`
|
||||
#[serde(default)]
|
||||
pub recreate: TerminalRecreateMode,
|
||||
}
|
||||
|
||||
fn default_command() -> String {
|
||||
String::from("bash")
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]
|
||||
#[response(NoData)]
|
||||
#[error(serror::Error)]
|
||||
pub struct DeleteTerminal {
|
||||
/// The name of the terminal to delete
|
||||
pub terminal: String,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]
|
||||
#[response(NoData)]
|
||||
#[error(serror::Error)]
|
||||
pub struct DeleteAllTerminals {}
|
||||
|
||||
//
|
||||
|
||||
/// Create a single use auth token to connect to periphery terminal websocket.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]
|
||||
#[response(CreateTerminalAuthTokenResponse)]
|
||||
#[error(serror::Error)]
|
||||
pub struct CreateTerminalAuthToken {}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct CreateTerminalAuthTokenResponse {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ConnectTerminalQuery {
|
||||
/// Use [CreateTerminalAuthToken] to create a single-use
|
||||
/// token to send in the query.
|
||||
pub token: String,
|
||||
/// Each periphery can keep multiple terminals open.
|
||||
/// If a terminal with the specified name already exists,
|
||||
/// it will be attached to.
|
||||
/// Otherwise a new terminal will be created,
|
||||
/// which will persist until it is either exited via command (ie `exit`),
|
||||
/// or deleted using [DeleteTerminal]
|
||||
pub terminal: String,
|
||||
/// Optional. The initial command to execute on connection to the shell.
|
||||
pub init: Option<String>,
|
||||
}
|
||||
@@ -8,6 +8,8 @@ use serde_json::json;
|
||||
|
||||
pub mod api;
|
||||
|
||||
mod terminal;
|
||||
|
||||
fn periphery_http_client() -> &'static reqwest::Client {
|
||||
static PERIPHERY_HTTP_CLIENT: OnceLock<reqwest::Client> =
|
||||
OnceLock::new();
|
||||
@@ -95,12 +97,12 @@ impl PeripheryClient {
|
||||
req.send().await.context("failed at request to periphery")?;
|
||||
let status = res.status();
|
||||
tracing::debug!(
|
||||
"got response | type: {req_type} | {status} | body: {res:?}",
|
||||
"got response | type: {req_type} | {status} | response: {res:?}",
|
||||
);
|
||||
if status == StatusCode::OK {
|
||||
tracing::debug!("response ok, deserializing");
|
||||
res.json().await.with_context(|| format!(
|
||||
"failed to parse response to json | type: {req_type} | body: {request:?}"
|
||||
"failed to parse response to json | type: {req_type} | request: {request:?}"
|
||||
))
|
||||
} else {
|
||||
tracing::debug!("response is non-200");
|
||||
|
||||
125
client/periphery/rs/src/terminal.rs
Normal file
125
client/periphery/rs/src/terminal.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
use rustls::{ClientConfig, client::danger::ServerCertVerifier};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_tungstenite::{Connector, MaybeTlsStream, WebSocketStream};
|
||||
|
||||
use crate::{
|
||||
PeripheryClient,
|
||||
api::terminal::{ConnectTerminalQuery, CreateTerminalAuthToken},
|
||||
};
|
||||
|
||||
impl PeripheryClient {
|
||||
/// Handles ws connect and login.
|
||||
/// Does not handle reconnect.
|
||||
pub async fn connect_terminal(
|
||||
&self,
|
||||
terminal: String,
|
||||
init: Option<String>,
|
||||
) -> anyhow::Result<WebSocketStream<MaybeTlsStream<TcpStream>>> {
|
||||
tracing::trace!(
|
||||
"request | type: ConnectTerminal | terminal name: {terminal} | init command: {init:?}",
|
||||
);
|
||||
|
||||
let token = self
|
||||
.request(CreateTerminalAuthToken {})
|
||||
.await
|
||||
.context("Failed to create terminal auth token")?;
|
||||
|
||||
let query_str = serde_qs::to_string(&ConnectTerminalQuery {
|
||||
token: token.token,
|
||||
terminal,
|
||||
init,
|
||||
})
|
||||
.context("Failed to serialize query string")?;
|
||||
|
||||
let url = format!(
|
||||
"{}/terminal?{query_str}",
|
||||
self.address.replacen("http", "ws", 1)
|
||||
);
|
||||
|
||||
let (stream, _) = if url.starts_with("wss") {
|
||||
tokio_tungstenite::connect_async_tls_with_config(
|
||||
url,
|
||||
None,
|
||||
false,
|
||||
Some(Connector::Rustls(Arc::new(
|
||||
ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(
|
||||
InsecureVerifier,
|
||||
))
|
||||
.with_no_client_auth(),
|
||||
))),
|
||||
)
|
||||
.await
|
||||
.context("failed to connect to websocket")?
|
||||
} else {
|
||||
tokio_tungstenite::connect_async(url)
|
||||
.await
|
||||
.context("failed to connect to websocket")?
|
||||
};
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InsecureVerifier;
|
||||
|
||||
impl ServerCertVerifier for InsecureVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &rustls::pki_types::CertificateDer<'_>,
|
||||
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
|
||||
_server_name: &rustls::pki_types::ServerName<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: rustls::pki_types::UnixTime,
|
||||
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error>
|
||||
{
|
||||
Ok(rustls::client::danger::ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &rustls::pki_types::CertificateDer<'_>,
|
||||
_dss: &rustls::DigitallySignedStruct,
|
||||
) -> Result<
|
||||
rustls::client::danger::HandshakeSignatureValid,
|
||||
rustls::Error,
|
||||
> {
|
||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &rustls::pki_types::CertificateDer<'_>,
|
||||
_dss: &rustls::DigitallySignedStruct,
|
||||
) -> Result<
|
||||
rustls::client::danger::HandshakeSignatureValid,
|
||||
rustls::Error,
|
||||
> {
|
||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
||||
vec![
|
||||
rustls::SignatureScheme::RSA_PKCS1_SHA1,
|
||||
rustls::SignatureScheme::ECDSA_SHA1_Legacy,
|
||||
rustls::SignatureScheme::RSA_PKCS1_SHA256,
|
||||
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
|
||||
rustls::SignatureScheme::RSA_PKCS1_SHA384,
|
||||
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
|
||||
rustls::SignatureScheme::RSA_PKCS1_SHA512,
|
||||
rustls::SignatureScheme::ECDSA_NISTP521_SHA512,
|
||||
rustls::SignatureScheme::RSA_PSS_SHA256,
|
||||
rustls::SignatureScheme::RSA_PSS_SHA384,
|
||||
rustls::SignatureScheme::RSA_PSS_SHA512,
|
||||
rustls::SignatureScheme::ED25519,
|
||||
rustls::SignatureScheme::ED448,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -120,11 +120,15 @@ KOMODO_HETZNER_TOKEN= # Alt: KOMODO_HETZNER_TOKEN_FILE
|
||||
## Full variable list + descriptions are available here:
|
||||
## 🦎 https://github.com/moghtech/komodo/blob/main/config/periphery.config.toml 🦎
|
||||
|
||||
## Specify the root directory used by Periphery agent.
|
||||
PERIPHERY_ROOT_DIRECTORY=/etc/komodo
|
||||
|
||||
## Periphery passkeys must include KOMODO_PASSKEY to authenticate.
|
||||
PERIPHERY_PASSKEYS=${KOMODO_PASSKEY}
|
||||
|
||||
## Specify the root directory used by Periphery agent.
|
||||
PERIPHERY_ROOT_DIRECTORY=/etc/komodo
|
||||
## Specify whether to disable the terminals feature
|
||||
## and disallow remote shell access (inside the Periphery container).
|
||||
PERIPHERY_DISABLE_TERMINALS=false
|
||||
|
||||
## Enable SSL using self signed certificates.
|
||||
## Connect to Periphery at https://address:8120.
|
||||
|
||||
@@ -18,6 +18,11 @@ services:
|
||||
PERIPHERY_ROOT_DIRECTORY: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}
|
||||
## Pass the same passkey as used by the Komodo Core connecting to this Periphery agent.
|
||||
PERIPHERY_PASSKEYS: abc123
|
||||
## Make server run over https
|
||||
PERIPHERY_SSL_ENABLED: true
|
||||
## Specify whether to disable the terminals feature
|
||||
## and disallow remote shell access (inside the Periphery container).
|
||||
PERIPHERY_DISABLE_TERMINALS: false
|
||||
## If the disk size is overreporting, can use one of these to
|
||||
## whitelist / blacklist the disks to filter them, whichever is easier.
|
||||
## Accepts comma separated list of paths.
|
||||
|
||||
@@ -48,10 +48,16 @@ root_directory = "/etc/komodo"
|
||||
## Default: ${root_directory}/builds
|
||||
# build_dir = "/etc/komodo/builds"
|
||||
|
||||
## Disable the terminal APIs and disallow remote shell access through Periphery.
|
||||
## Env: PERIPHERY_DISABLE_TERMINALS
|
||||
## Default: false
|
||||
disable_terminals = false
|
||||
|
||||
## How often Periphery polls the host for system stats,
|
||||
## like CPU / memory usage.
|
||||
## like CPU / memory usage. To effectively disable polling,
|
||||
## set this to something like 1-hr.
|
||||
## Env: PERIPHERY_STATS_POLLING_RATE
|
||||
## Options: 1-sec, 5-sec, 10-sec, 30-sec, 1-min
|
||||
## Options: 1-sec, 5-sec, 10-sec, 30-sec, 1-min, 5-min, 30-min, 1-hr
|
||||
## Default: 5-sec
|
||||
stats_polling_rate = "5-sec"
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ Connecting a server to Komodo has 2 steps:
|
||||
|
||||
## Install Periphery
|
||||
|
||||
You can install Periphery as a systemd managed process, run it as a [docker container](https://github.com/moghtech/komodo/pkgs/container/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 do whatever you want with the binary.
|
||||
|
||||
:::warning
|
||||
Allowing unintended access to the Periphery agent API is a security risk.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
- [FAQ, Tips, and Tricks](https://blog.foxxmd.dev/posts/komodo-tips-tricks) by [FoxxMD](https://github.com/FoxxMD)
|
||||
- [Compose Environments Explained](https://blog.foxxmd.dev/posts/compose-envs-explained) by [FoxxMD](https://github.com/FoxxMD)
|
||||
- [How To: Automate version updates for your self-hosted Docker containers with Gitea, Renovate, and Komodo](https://nickcunningh.am/blog/how-to-automate-version-updates-for-your-self-hosted-docker-containers-with-gitea-renovate-and-komodo) by [TheNickOfTime](https://github.com/TheNickOfTime)
|
||||
|
||||
- [Setting up Komodo, comparison to Portainer, and FAQ](https://skyblog.one/komodo-the-better-alternative-to-portainer-for-container-management) by [Skyfay](https://skyblog.one/authors/)
|
||||
### Community Alerters
|
||||
These provide alerting implementations which can be used with the `Custom` Alerter type.
|
||||
- [Discord](https://github.com/FoxxMD/deploy-discord-alerter) by [FoxxMD](https://github.com/FoxxMD)
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
"@radix-ui/react-toggle-group": "1.1.2",
|
||||
"@tanstack/react-query": "5.67.3",
|
||||
"@tanstack/react-table": "8.21.2",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"ansi-to-html": "0.7.2",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
@@ -44,6 +46,7 @@
|
||||
"react-dom": "19.0.0",
|
||||
"react-minimal-pie-chart": "9.1.0",
|
||||
"react-router-dom": "7.3.0",
|
||||
"react-xtermjs": "^1.0.10",
|
||||
"sanitize-html": "2.14.0",
|
||||
"tailwind-merge": "2.6.0",
|
||||
"tailwindcss-animate": "1.0.7"
|
||||
|
||||
13
frontend/public/client/lib.d.ts
vendored
13
frontend/public/client/lib.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import { AuthResponses, ExecuteResponses, ReadResponses, UserResponses, WriteResponses } from "./responses.js";
|
||||
import { AuthRequest, ExecuteRequest, ReadRequest, Update, UpdateListItem, UserRequest, WriteRequest } from "./types.js";
|
||||
import { AuthRequest, ConnectTerminalQuery, ExecuteRequest, ReadRequest, Update, UpdateListItem, UserRequest, WriteRequest } from "./types.js";
|
||||
export * as Types from "./types.js";
|
||||
type InitOptions = {
|
||||
type: "jwt";
|
||||
@@ -132,4 +132,15 @@ export declare function KomodoClient(url: string, options: InitOptions): {
|
||||
cancel?: CancelToken;
|
||||
on_cancel?: () => void;
|
||||
}) => Promise<void>;
|
||||
/**
|
||||
* Subscribes to terminal io over websocket message,
|
||||
* for use with xtermjs.
|
||||
*/
|
||||
connect_terminal: ({ query, on_message, on_login, on_open, on_close, }: {
|
||||
query: ConnectTerminalQuery;
|
||||
on_message?: (e: MessageEvent<any>) => void;
|
||||
on_login?: () => void;
|
||||
on_open?: () => void;
|
||||
on_close?: () => void;
|
||||
}) => WebSocket;
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ export function KomodoClient(url, options) {
|
||||
key: options.type === "api-key" ? options.params.key : undefined,
|
||||
secret: options.type === "api-key" ? options.params.secret : undefined,
|
||||
};
|
||||
const request = async (path, request) => new Promise(async (res, rej) => {
|
||||
const request = (path, request) => new Promise(async (res, rej) => {
|
||||
try {
|
||||
let response = await fetch(url + path, {
|
||||
method: "POST",
|
||||
@@ -155,6 +155,42 @@ export function KomodoClient(url, options) {
|
||||
}
|
||||
}
|
||||
};
|
||||
const connect_terminal = ({ query, on_message, on_login, on_open, on_close, }) => {
|
||||
const url_query = new URLSearchParams(query).toString();
|
||||
const ws = new WebSocket(url.replace("http", "ws") + "/ws/terminal?" + url_query);
|
||||
// Handle login on websocket open
|
||||
ws.onopen = () => {
|
||||
const login_msg = options.type === "jwt"
|
||||
? {
|
||||
type: "Jwt",
|
||||
params: {
|
||||
jwt: options.params.jwt,
|
||||
},
|
||||
}
|
||||
: {
|
||||
type: "ApiKeys",
|
||||
params: {
|
||||
key: options.params.key,
|
||||
secret: options.params.secret,
|
||||
},
|
||||
};
|
||||
ws.send(JSON.stringify(login_msg));
|
||||
on_open?.();
|
||||
};
|
||||
ws.onmessage = (e) => {
|
||||
if (e.data == "LOGGED_IN") {
|
||||
ws.binaryType = "arraybuffer";
|
||||
ws.onmessage = (e) => on_message?.(e);
|
||||
on_login?.();
|
||||
return;
|
||||
}
|
||||
else {
|
||||
on_message?.(e);
|
||||
}
|
||||
};
|
||||
ws.onclose = () => on_close?.();
|
||||
return ws;
|
||||
};
|
||||
return {
|
||||
/**
|
||||
* Call the `/auth` api.
|
||||
@@ -245,5 +281,10 @@ export function KomodoClient(url, options) {
|
||||
* Note. Awaiting this method will never finish.
|
||||
*/
|
||||
subscribe_to_update_websocket,
|
||||
/**
|
||||
* Subscribes to terminal io over websocket message,
|
||||
* for use with xtermjs.
|
||||
*/
|
||||
connect_terminal,
|
||||
};
|
||||
}
|
||||
|
||||
4
frontend/public/client/responses.d.ts
vendored
4
frontend/public/client/responses.d.ts
vendored
@@ -65,6 +65,7 @@ export type ReadResponses = {
|
||||
GetHistoricalServerStats: Types.GetHistoricalServerStatsResponse;
|
||||
ListServers: Types.ListServersResponse;
|
||||
ListFullServers: Types.ListFullServersResponse;
|
||||
ListTerminals: Types.ListTerminalsResponse;
|
||||
GetDeploymentsSummary: Types.GetDeploymentsSummaryResponse;
|
||||
GetDeployment: Types.GetDeploymentResponse;
|
||||
GetDeploymentContainer: Types.GetDeploymentContainerResponse;
|
||||
@@ -158,6 +159,9 @@ export type WriteResponses = {
|
||||
UpdateServer: Types.Server;
|
||||
RenameServer: Types.Update;
|
||||
CreateNetwork: Types.Update;
|
||||
CreateTerminal: Types.NoData;
|
||||
DeleteTerminal: Types.NoData;
|
||||
DeleteAllTerminals: Types.NoData;
|
||||
CreateDeployment: Types.Deployment;
|
||||
CopyDeployment: Types.Deployment;
|
||||
CreateDeploymentFromContainer: Types.Deployment;
|
||||
|
||||
115
frontend/public/client/types.d.ts
vendored
115
frontend/public/client/types.d.ts
vendored
@@ -2106,6 +2106,8 @@ export interface SystemInformation {
|
||||
host_name?: string;
|
||||
/** The CPU's brand */
|
||||
cpu_brand: string;
|
||||
/** Whether terminals are disabled on this Periphery */
|
||||
terminals_disabled: boolean;
|
||||
}
|
||||
export type GetSystemInformationResponse = SystemInformation;
|
||||
/** Info for a single disk mounted on the system. */
|
||||
@@ -3514,6 +3516,8 @@ export interface ServerListItemInfo {
|
||||
send_mem_alerts: boolean;
|
||||
/** Whether server is configured to send disk alerts. */
|
||||
send_disk_alerts: boolean;
|
||||
/** Whether terminals are disabled for this Server. */
|
||||
terminals_disabled: boolean;
|
||||
}
|
||||
export type ServerListItem = ResourceListItem<ServerListItemInfo>;
|
||||
export type ListServersResponse = ServerListItem[];
|
||||
@@ -3625,6 +3629,19 @@ export interface SystemProcess {
|
||||
}
|
||||
export type ListSystemProcessesResponse = SystemProcess[];
|
||||
export type ListTagsResponse = Tag[];
|
||||
/**
|
||||
* Info about an active terminal on a server.
|
||||
* Retrieve with [ListTerminals][crate::api::read::server::ListTerminals].
|
||||
*/
|
||||
export interface TerminalInfo {
|
||||
/** 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;
|
||||
}
|
||||
export type ListTerminalsResponse = TerminalInfo[];
|
||||
export type ListUserGroupsResponse = UserGroup[];
|
||||
export type ListUserTargetPermissionsResponse = Permission[];
|
||||
export type ListUsersResponse = User[];
|
||||
@@ -4090,6 +4107,25 @@ export interface CommitSync {
|
||||
/** Id or name */
|
||||
sync: string;
|
||||
}
|
||||
/**
|
||||
* Query to connect to a terminal (interactive shell over websocket) on the given server.
|
||||
* TODO: Document calling.
|
||||
*/
|
||||
export interface ConnectTerminalQuery {
|
||||
/** Server Id or name */
|
||||
server: string;
|
||||
/**
|
||||
* Each periphery can keep multiple terminals open.
|
||||
* If a terminals with the specified name already exists,
|
||||
* it will be attached to.
|
||||
* Otherwise a new terminal will be created for the command,
|
||||
* which will persist until it is deleted using
|
||||
* [DeleteTerminal][crate::api::write::server::DeleteTerminal]
|
||||
*/
|
||||
terminal: string;
|
||||
/** Optional. The initial command to execute on connection to the shell. */
|
||||
init?: string;
|
||||
}
|
||||
export interface Conversion {
|
||||
/** reference on the server. */
|
||||
local: string;
|
||||
@@ -4449,6 +4485,42 @@ export interface CreateTag {
|
||||
/** The name of the tag. */
|
||||
name: string;
|
||||
}
|
||||
/**
|
||||
* Configures the behavior of [CreateTerminal] if the
|
||||
* specified terminal name already exists.
|
||||
*/
|
||||
export declare enum TerminalRecreateMode {
|
||||
/**
|
||||
* Never kill the old terminal if it already exists.
|
||||
* If the command is different, returns error.
|
||||
*/
|
||||
Never = "Never",
|
||||
/** Always kill the old terminal and create new one */
|
||||
Always = "Always",
|
||||
/** Only kill and recreate if the command is different. */
|
||||
DifferentCommand = "DifferentCommand"
|
||||
}
|
||||
/**
|
||||
* Create a terminal on the server.
|
||||
* Response: [NoData]
|
||||
*/
|
||||
export interface CreateTerminal {
|
||||
/** Server Id or name */
|
||||
server: string;
|
||||
/** The name of the terminal on the server to create. */
|
||||
name: string;
|
||||
/**
|
||||
* The shell command (eg `bash`) to init the shell.
|
||||
*
|
||||
* This can also include args:
|
||||
* `docker exec -it container sh`
|
||||
*
|
||||
* Default: `bash`
|
||||
*/
|
||||
command: string;
|
||||
/** Default: `Never` */
|
||||
recreate?: TerminalRecreateMode;
|
||||
}
|
||||
/** **Admin only.** Create a user group. Response: [UserGroup] */
|
||||
export interface CreateUserGroup {
|
||||
/** The name to assign to the new UserGroup */
|
||||
@@ -4494,6 +4566,14 @@ export interface DeleteAlerter {
|
||||
/** The id or name of the alerter to delete. */
|
||||
id: string;
|
||||
}
|
||||
/**
|
||||
* Delete all terminals on the server.
|
||||
* Response: [NoData]
|
||||
*/
|
||||
export interface DeleteAllTerminals {
|
||||
/** Server Id or name */
|
||||
server: string;
|
||||
}
|
||||
/**
|
||||
* Delete an api key for the calling user.
|
||||
* Response: [NoData]
|
||||
@@ -4667,6 +4747,16 @@ export interface DeleteTag {
|
||||
/** The id of the tag to delete. */
|
||||
id: string;
|
||||
}
|
||||
/**
|
||||
* Delete a terminal on the server.
|
||||
* Response: [NoData]
|
||||
*/
|
||||
export interface DeleteTerminal {
|
||||
/** Server Id or name */
|
||||
server: string;
|
||||
/** The name of the terminal on the server to delete. */
|
||||
terminal: string;
|
||||
}
|
||||
/**
|
||||
* **Admin only**. Delete a user.
|
||||
* Admins can delete any non-admin user.
|
||||
@@ -6092,6 +6182,19 @@ export interface ListSystemProcesses {
|
||||
export interface ListTags {
|
||||
query?: MongoDocument;
|
||||
}
|
||||
/**
|
||||
* List the current terminals on specified server.
|
||||
* Response: [ListTerminalsResponse].
|
||||
*/
|
||||
export interface ListTerminals {
|
||||
/** Id or name */
|
||||
server: string;
|
||||
/**
|
||||
* Force a fresh call to Periphery for the list.
|
||||
* Otherwise the response will be cached for 30s
|
||||
*/
|
||||
fresh?: boolean;
|
||||
}
|
||||
/**
|
||||
* Paginated endpoint for updates matching optional query.
|
||||
* More recent updates will be returned first.
|
||||
@@ -7646,6 +7749,9 @@ export type ReadRequest = {
|
||||
} | {
|
||||
type: "ListComposeProjects";
|
||||
params: ListComposeProjects;
|
||||
} | {
|
||||
type: "ListTerminals";
|
||||
params: ListTerminals;
|
||||
} | {
|
||||
type: "GetDeploymentsSummary";
|
||||
params: GetDeploymentsSummary;
|
||||
@@ -7930,6 +8036,15 @@ export type WriteRequest = {
|
||||
} | {
|
||||
type: "CreateNetwork";
|
||||
params: CreateNetwork;
|
||||
} | {
|
||||
type: "CreateTerminal";
|
||||
params: CreateTerminal;
|
||||
} | {
|
||||
type: "DeleteTerminal";
|
||||
params: DeleteTerminal;
|
||||
} | {
|
||||
type: "DeleteAllTerminals";
|
||||
params: DeleteAllTerminals;
|
||||
} | {
|
||||
type: "CreateDeployment";
|
||||
params: CreateDeployment;
|
||||
|
||||
@@ -489,6 +489,22 @@ export var SyncWebhookAction;
|
||||
SyncWebhookAction["Refresh"] = "Refresh";
|
||||
SyncWebhookAction["Sync"] = "Sync";
|
||||
})(SyncWebhookAction || (SyncWebhookAction = {}));
|
||||
/**
|
||||
* Configures the behavior of [CreateTerminal] if the
|
||||
* specified terminal name already exists.
|
||||
*/
|
||||
export var TerminalRecreateMode;
|
||||
(function (TerminalRecreateMode) {
|
||||
/**
|
||||
* Never kill the old terminal if it already exists.
|
||||
* If the command is different, returns error.
|
||||
*/
|
||||
TerminalRecreateMode["Never"] = "Never";
|
||||
/** Always kill the old terminal and create new one */
|
||||
TerminalRecreateMode["Always"] = "Always";
|
||||
/** Only kill and recreate if the command is different. */
|
||||
TerminalRecreateMode["DifferentCommand"] = "DifferentCommand";
|
||||
})(TerminalRecreateMode || (TerminalRecreateMode = {}));
|
||||
export var HetznerDatacenter;
|
||||
(function (HetznerDatacenter) {
|
||||
HetznerDatacenter["Nuremberg1Dc3"] = "Nuremberg1Dc3";
|
||||
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/tooltip";
|
||||
import { DeploymentTerminal } from "./terminal";
|
||||
import { useEditPermissions } from "@pages/resource";
|
||||
|
||||
// const configOrLog = atomWithStorage("config-or-log-v1", "Config");
|
||||
|
||||
@@ -43,57 +45,68 @@ export const useFullDeployment = (id: string) =>
|
||||
useRead("GetDeployment", { deployment: id }, { refetchInterval: 10_000 })
|
||||
.data;
|
||||
|
||||
const ConfigOrLog = ({ id }: { id: string }) => {
|
||||
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("deployment-tabs-v1", "Config");
|
||||
const state = useDeployment(id)?.info.state;
|
||||
const [_view, setView] = useLocalStorage<"Config" | "Log" | "Terminal">(
|
||||
"deployment-tabs-v1",
|
||||
"Config"
|
||||
);
|
||||
const { canWrite: canWriteServer } = useEditPermissions({
|
||||
type: "Server",
|
||||
id: deployment.info.server_id,
|
||||
});
|
||||
const terminals_disabled =
|
||||
useServer(deployment.info.server_id)?.info.terminals_disabled ?? true;
|
||||
const state = deployment.info.state;
|
||||
const logsDisabled =
|
||||
state === undefined ||
|
||||
state === Types.DeploymentState.Unknown ||
|
||||
state === Types.DeploymentState.NotDeployed;
|
||||
const terminalDisabled =
|
||||
!canWriteServer ||
|
||||
terminals_disabled ||
|
||||
state !== Types.DeploymentState.Running;
|
||||
const view =
|
||||
(logsDisabled && _view === "Log") ||
|
||||
(terminalDisabled && _view === "Terminal")
|
||||
? "Config"
|
||||
: _view;
|
||||
|
||||
const tabs = (
|
||||
<TabsList className="justify-start w-fit">
|
||||
<TabsTrigger value="Config" className="w-[110px]">
|
||||
Config
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="Log" className="w-[110px]" disabled={logsDisabled}>
|
||||
Log
|
||||
</TabsTrigger>
|
||||
{!terminalDisabled && (
|
||||
<TabsTrigger value="Terminal" className="w-[110px]">
|
||||
Terminal
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
);
|
||||
return (
|
||||
<Tabs
|
||||
value={logsDisabled ? "Config" : view}
|
||||
onValueChange={setView}
|
||||
className="grid gap-4"
|
||||
>
|
||||
<Tabs value={view} onValueChange={setView as any} className="grid gap-4">
|
||||
<TabsContent value="Config">
|
||||
<DeploymentConfig
|
||||
id={id}
|
||||
titleOther={
|
||||
<TabsList className="justify-start w-fit">
|
||||
<TabsTrigger value="Config" className="w-[110px]">
|
||||
Config
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="Log"
|
||||
className="w-[110px]"
|
||||
disabled={logsDisabled}
|
||||
>
|
||||
Log
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
}
|
||||
/>
|
||||
<DeploymentConfig id={deployment.id} titleOther={tabs} />
|
||||
</TabsContent>
|
||||
<TabsContent value="Log">
|
||||
<DeploymentLogs
|
||||
id={id}
|
||||
titleOther={
|
||||
<TabsList className="justify-start w-fit">
|
||||
<TabsTrigger value="Config" className="w-[110px]">
|
||||
Config
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="Log"
|
||||
className="w-[110px]"
|
||||
disabled={logsDisabled}
|
||||
>
|
||||
Log
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
}
|
||||
/>
|
||||
<DeploymentLogs id={deployment.id} titleOther={tabs} />
|
||||
</TabsContent>
|
||||
<TabsContent value="Terminal">
|
||||
<DeploymentTerminal deployment={deployment} titleOther={tabs} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
@@ -272,7 +285,7 @@ export const DeploymentComponents: RequiredResourceComponents = {
|
||||
|
||||
Page: {},
|
||||
|
||||
Config: ConfigOrLog,
|
||||
Config: ConfigTabs,
|
||||
|
||||
DangerZone: ({ id }) => (
|
||||
<>
|
||||
|
||||
21
frontend/src/components/resources/deployment/terminal.tsx
Normal file
21
frontend/src/components/resources/deployment/terminal.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ReactNode } from "react";
|
||||
import { ContainerTerminal } from "@components/terminal";
|
||||
import { Types } from "komodo_client";
|
||||
|
||||
export const DeploymentTerminal = ({
|
||||
deployment,
|
||||
titleOther,
|
||||
}: {
|
||||
deployment: Types.DeploymentListItem;
|
||||
titleOther?: ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
deployment.info.server_id && (
|
||||
<ContainerTerminal
|
||||
titleOther={titleOther}
|
||||
server={deployment.info.server_id}
|
||||
container_name={deployment.name}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -38,6 +38,9 @@ import { ServerInfo } from "./info";
|
||||
import { ServerStats } from "./stats";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
import { ServerTerminals } from "./terminal";
|
||||
import { useEditPermissions } from "@pages/resource";
|
||||
import { Card, CardHeader, CardTitle } from "@ui/card";
|
||||
|
||||
export const useServer = (id?: string) =>
|
||||
useRead("ListServers", {}, { refetchInterval: 10_000 }).data?.find(
|
||||
@@ -59,12 +62,14 @@ const Icon = ({ id, size }: { id?: string; size: number }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ConfigStatsDockerResources = ({ id }: { id: string }) => {
|
||||
const ConfigTabs = ({ id }: { id: string }) => {
|
||||
const [view, setView] = useLocalStorage<
|
||||
"Config" | "Stats" | "Docker" | "Resources"
|
||||
"Config" | "Stats" | "Docker" | "Resources" | "Terminals"
|
||||
>(`server-${id}-tab`, "Config");
|
||||
|
||||
const is_admin = useUser().data?.admin ?? false;
|
||||
const { canWrite } = useEditPermissions({ type: "Server", id });
|
||||
const terminals_disabled = useServer(id)?.info.terminals_disabled ?? true;
|
||||
const disable_non_admin_create =
|
||||
useRead("GetCoreInfo", {}).data?.disable_non_admin_create ?? true;
|
||||
|
||||
@@ -109,6 +114,12 @@ const ConfigStatsDockerResources = ({ id }: { id: string }) => {
|
||||
>
|
||||
Resources
|
||||
</TabsTrigger>
|
||||
|
||||
{!terminals_disabled && canWrite && (
|
||||
<TabsTrigger value="Terminals" className="w-[110px]">
|
||||
Terminals
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
);
|
||||
return (
|
||||
@@ -163,6 +174,32 @@ const ConfigStatsDockerResources = ({ id }: { id: string }) => {
|
||||
</Section>
|
||||
</Section>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="Terminals">
|
||||
{!terminals_disabled && canWrite && (
|
||||
<ServerTerminals id={id} titleOther={tabsList} />
|
||||
)}
|
||||
{terminals_disabled && canWrite && (
|
||||
<Section titleOther={tabsList}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Terminals are disabled on this Server.</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</Section>
|
||||
)}
|
||||
{!canWrite && (
|
||||
<Section titleOther={tabsList}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
User does not have permission to use Terminals.
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</Section>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
@@ -451,7 +488,7 @@ export const ServerComponents: RequiredResourceComponents = {
|
||||
|
||||
Page: {},
|
||||
|
||||
Config: ConfigStatsDockerResources,
|
||||
Config: ConfigTabs,
|
||||
|
||||
DangerZone: ({ id }) => (
|
||||
<>
|
||||
|
||||
143
frontend/src/components/resources/server/terminal.tsx
Normal file
143
frontend/src/components/resources/server/terminal.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { Section } from "@components/layouts";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { useLocalStorage, useRead, useWrite } from "@lib/hooks";
|
||||
import { Card, CardContent, CardHeader } from "@ui/card";
|
||||
import { Badge } from "@ui/badge";
|
||||
import { Button } from "@ui/button";
|
||||
import { Loader2, Plus, RefreshCcw, X } from "lucide-react";
|
||||
import { Terminal } from "@components/terminal";
|
||||
|
||||
export const ServerTerminals = ({
|
||||
id,
|
||||
titleOther,
|
||||
}: {
|
||||
id: string;
|
||||
titleOther?: ReactNode;
|
||||
}) => {
|
||||
const { data: terminals, refetch: refetchTerminals } = useRead(
|
||||
"ListTerminals",
|
||||
{
|
||||
server: id,
|
||||
fresh: true,
|
||||
},
|
||||
{
|
||||
refetchInterval: 5000,
|
||||
}
|
||||
);
|
||||
const { mutateAsync: create_terminal, isPending: create_pending } =
|
||||
useWrite("CreateTerminal");
|
||||
const { mutateAsync: delete_terminal } = useWrite("DeleteTerminal");
|
||||
const [_selected, setSelected] = useLocalStorage<{
|
||||
selected: string | undefined;
|
||||
}>(`server-${id}-selected-terminal-v1`, { selected: undefined });
|
||||
|
||||
const selected =
|
||||
_selected.selected ??
|
||||
terminals?.[0]?.name ??
|
||||
next_terminal_name(terminals?.map((t) => t.name) ?? []);
|
||||
|
||||
const [_reconnect, _setReconnect] = useState(false);
|
||||
const triggerReconnect = () => _setReconnect((r) => !r);
|
||||
|
||||
const create = async () => {
|
||||
if (!terminals) return;
|
||||
const name = next_terminal_name(terminals.map((t) => t.name));
|
||||
await create_terminal({
|
||||
server: id,
|
||||
name,
|
||||
command: "bash",
|
||||
});
|
||||
refetchTerminals();
|
||||
setTimeout(() => {
|
||||
setSelected({
|
||||
selected: name,
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (terminals && terminals.length === 0) {
|
||||
create();
|
||||
}
|
||||
}, [terminals]);
|
||||
|
||||
return (
|
||||
<Section titleOther={titleOther}>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row gap-4 items-center justify-between">
|
||||
<div className="flex gap-4">
|
||||
{terminals?.map(({ name: terminal }) => (
|
||||
<Badge
|
||||
key={terminal}
|
||||
variant={terminal === selected ? "default" : "secondary"}
|
||||
className="w-fit min-w-[150px] px-2 py-1 cursor-pointer flex gap-4 justify-between"
|
||||
onClick={() => setSelected({ selected: terminal })}
|
||||
>
|
||||
{terminal}
|
||||
<Button
|
||||
className="p-1 h-fit"
|
||||
variant="destructive"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await delete_terminal({ server: id, terminal });
|
||||
refetchTerminals();
|
||||
if (selected === terminal) {
|
||||
setSelected({ selected: undefined });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</Badge>
|
||||
))}
|
||||
{terminals && (
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
variant="outline"
|
||||
onClick={create}
|
||||
disabled={create_pending}
|
||||
>
|
||||
New Terminal
|
||||
{create_pending ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Plus className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
variant="secondary"
|
||||
onClick={() => triggerReconnect()}
|
||||
>
|
||||
Reconnect
|
||||
<RefreshCcw className="w-4 h-4" />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="min-h-[65vh]">
|
||||
{terminals?.map(({ name: terminal }) => (
|
||||
<Terminal
|
||||
key={terminal}
|
||||
server={id}
|
||||
terminal={terminal}
|
||||
selected={selected === terminal}
|
||||
_reconnect={_reconnect}
|
||||
/>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const next_terminal_name = (terminal_names: string[]) => {
|
||||
for (let i = 1; i <= terminal_names.length + 1; i++) {
|
||||
const name = `terminal ${i}`;
|
||||
if (!terminal_names.includes(name)) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
// This shouldn't happen
|
||||
return `terminal -1`;
|
||||
};
|
||||
282
frontend/src/components/terminal.tsx
Normal file
282
frontend/src/components/terminal.tsx
Normal file
@@ -0,0 +1,282 @@
|
||||
import { komodo_client, useLocalStorage, useWrite } from "@lib/hooks";
|
||||
import { cn } from "@lib/utils";
|
||||
import { useTheme } from "@ui/theme";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { ITheme } from "@xterm/xterm";
|
||||
import { Types } from "komodo_client";
|
||||
import { ReactNode, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useXTerm, UseXTermProps } from "react-xtermjs";
|
||||
import { Section } from "./layouts";
|
||||
import { CardTitle } from "@ui/card";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@ui/select";
|
||||
import { Input } from "@ui/input";
|
||||
import { Button } from "@ui/button";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
|
||||
const BASE_SHELLS = ["sh", "bash"];
|
||||
|
||||
export const ContainerTerminal = ({
|
||||
server,
|
||||
container_name,
|
||||
titleOther,
|
||||
}: {
|
||||
server: string;
|
||||
container_name: string;
|
||||
titleOther?: ReactNode;
|
||||
}) => {
|
||||
const { mutateAsync: create_terminal } = useWrite("CreateTerminal");
|
||||
const [_reconnect, _setReconnect] = useState(false);
|
||||
const triggerReconnect = () => _setReconnect((r) => !r);
|
||||
const [_clear, _setClear] = useState(false);
|
||||
const [shell, setShell] = useLocalStorage(
|
||||
`server-${server}-${container_name}-shell-v1`,
|
||||
"sh"
|
||||
);
|
||||
const [otherShell, setOtherShell] = useState("");
|
||||
let command = `docker exec -it ${container_name} ${shell}`;
|
||||
|
||||
useEffect(() => {
|
||||
create_terminal({
|
||||
server,
|
||||
name: container_name,
|
||||
command,
|
||||
recreate: Types.TerminalRecreateMode.DifferentCommand,
|
||||
}).then(() => _setClear((c) => !c));
|
||||
}, [server, container_name, shell]);
|
||||
|
||||
return (
|
||||
<Section
|
||||
titleOther={titleOther}
|
||||
actions={
|
||||
<div className="flex items-center gap-4 mr-[16px]">
|
||||
<CardTitle className="text-muted-foreground flex items-center gap-2">
|
||||
docker exec -it {container_name}
|
||||
<Select value={shell} onValueChange={setShell}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{[
|
||||
...BASE_SHELLS,
|
||||
...(!BASE_SHELLS.includes(shell) ? [shell] : []),
|
||||
].map((shell) => (
|
||||
<SelectItem key={shell} value={shell}>
|
||||
{shell}
|
||||
</SelectItem>
|
||||
))}
|
||||
<Input
|
||||
placeholder="other"
|
||||
value={otherShell}
|
||||
onChange={(e) => setOtherShell(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
setShell(otherShell);
|
||||
setOtherShell("");
|
||||
} else {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</CardTitle>
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
variant="secondary"
|
||||
onClick={() => triggerReconnect()}
|
||||
>
|
||||
Reconnect
|
||||
<RefreshCcw className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="min-h-[65vh]">
|
||||
<Terminal
|
||||
server={server}
|
||||
terminal={container_name}
|
||||
selected={true}
|
||||
command={command}
|
||||
_clear={_clear}
|
||||
_reconnect={_reconnect}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
export const Terminal = ({
|
||||
server,
|
||||
terminal,
|
||||
selected,
|
||||
command,
|
||||
_clear,
|
||||
_reconnect,
|
||||
}: {
|
||||
server: string;
|
||||
/** The terminal name to connect to */
|
||||
terminal: string;
|
||||
selected: boolean;
|
||||
/** Pass the command to reconnect if it changes */
|
||||
command?: string;
|
||||
_clear?: boolean;
|
||||
_reconnect: boolean;
|
||||
}) => {
|
||||
const { theme: __theme } = useTheme();
|
||||
const _theme =
|
||||
__theme === "system"
|
||||
? window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: __theme;
|
||||
const theme = _theme === "dark" ? DARK_THEME : LIGHT_THEME;
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const fitRef = useRef<FitAddon>(new FitAddon());
|
||||
|
||||
const resize = () => {
|
||||
fitRef.current.fit();
|
||||
if (term) {
|
||||
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
||||
const json = JSON.stringify({
|
||||
rows: term.rows,
|
||||
cols: term.cols,
|
||||
});
|
||||
const buf = new Uint8Array(json.length + 1);
|
||||
buf[0] = 0xff; // resize prefix
|
||||
for (let i = 0; i < json.length; i++) buf[i + 1] = json.charCodeAt(i);
|
||||
wsRef.current.send(buf);
|
||||
}
|
||||
term.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const onStdin = (data: string) => {
|
||||
// This is data user writes to stdin
|
||||
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
const buf = new Uint8Array(data.length + 1);
|
||||
buf[0] = 0x00; // data prefix
|
||||
for (let i = 0; i < data.length; i++) buf[i + 1] = data.charCodeAt(i);
|
||||
wsRef.current.send(buf);
|
||||
};
|
||||
|
||||
useEffect(resize, [selected]);
|
||||
|
||||
const params: UseXTermProps = useMemo(
|
||||
() => ({
|
||||
options: {
|
||||
convertEol: false,
|
||||
cursorBlink: true,
|
||||
cursorStyle: "block",
|
||||
fontFamily: "monospace",
|
||||
scrollback: 5000,
|
||||
// This is handled in ws on_message handler
|
||||
scrollOnUserInput: false,
|
||||
theme,
|
||||
},
|
||||
listeners: {
|
||||
onResize: resize,
|
||||
onData: onStdin,
|
||||
},
|
||||
addons: [fitRef.current],
|
||||
}),
|
||||
[theme]
|
||||
);
|
||||
|
||||
const { instance: term, ref: termRef } = useXTerm(params);
|
||||
|
||||
const viewport = (term as any)?._core?.viewport?._viewportElement as
|
||||
| HTMLDivElement
|
||||
| undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (!term || !viewport) return;
|
||||
|
||||
let delta = 0;
|
||||
term.attachCustomWheelEventHandler((e) => {
|
||||
e.preventDefault();
|
||||
// This is used to make touchpad and mousewheel more similar
|
||||
delta += Math.sign(e.deltaY) * Math.sqrt(Math.abs(e.deltaY)) * 20;
|
||||
return false;
|
||||
});
|
||||
const int = setInterval(() => {
|
||||
if (Math.abs(delta) < 1) return;
|
||||
viewport.scrollTop += delta;
|
||||
delta = 0;
|
||||
}, 100);
|
||||
return () => clearInterval(int);
|
||||
}, [term, termRef.current]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selected || !term) return;
|
||||
|
||||
term.clear();
|
||||
|
||||
let debounce = -1;
|
||||
|
||||
const ws = komodo_client().connect_terminal({
|
||||
query: {
|
||||
server,
|
||||
terminal,
|
||||
},
|
||||
on_login: () => {
|
||||
// console.log("logged in terminal");
|
||||
},
|
||||
on_open: resize,
|
||||
on_message: (e) => {
|
||||
term.write(new Uint8Array(e.data as ArrayBuffer), () => {
|
||||
if (viewport) {
|
||||
viewport.scrollTop = viewport.scrollHeight - viewport.clientHeight;
|
||||
}
|
||||
clearTimeout(debounce);
|
||||
debounce = setTimeout(() => {
|
||||
if (!viewport) return;
|
||||
viewport.scrollTop = viewport.scrollHeight - viewport.clientHeight;
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
on_close: () => {
|
||||
term.writeln("\r\n\x1b[33m[connection closed]\x1b[0m");
|
||||
},
|
||||
});
|
||||
|
||||
wsRef.current = ws;
|
||||
|
||||
return () => {
|
||||
ws.close();
|
||||
wsRef.current = null;
|
||||
};
|
||||
}, [term, viewport, selected, command, _reconnect]);
|
||||
|
||||
useEffect(() => term?.clear(), [_clear]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={termRef}
|
||||
className={cn("w-full h-[65vh]", selected ? "" : "hidden")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const LIGHT_THEME: ITheme = {
|
||||
background: "#f7f8f9",
|
||||
foreground: "#24292e",
|
||||
cursor: "#24292e",
|
||||
selectionBackground: "#c8d9fa",
|
||||
};
|
||||
|
||||
const DARK_THEME: ITheme = {
|
||||
background: "#151b25",
|
||||
foreground: "#f6f8fa",
|
||||
cursor: "#ffffff",
|
||||
selectionBackground: "#6e778a",
|
||||
};
|
||||
@@ -92,6 +92,10 @@
|
||||
position: absolute !important;
|
||||
}
|
||||
|
||||
.asdfa {
|
||||
color: #151b25
|
||||
.xterm {
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
.xterm .xterm-viewport::-webkit-scrollbar {
|
||||
@apply w-[16px] rounded-sm
|
||||
}
|
||||
@@ -108,12 +108,15 @@ export const useManageUser = <
|
||||
return useMutation({
|
||||
mutationKey: [type],
|
||||
mutationFn: (params: P) => komodo_client().user<T, R>(type, params),
|
||||
onError: (e: { result: { error?: string } }, v, c) => {
|
||||
onError: (e: { result: { error?: string; trace?: string[] } }, v, c) => {
|
||||
console.log("Auth error:", e);
|
||||
const msg = e.result.error ?? "Unknown error. See console.";
|
||||
let msg_log = msg ? msg + " | " : "";
|
||||
if (msg_log) {
|
||||
msg_log = msg_log[0].toUpperCase() + msg_log.slice(1);
|
||||
const msg = e.result?.error ?? "Unknown error. See console.";
|
||||
const detail = e.result?.trace
|
||||
?.map((msg) => msg[0].toUpperCase() + msg.slice(1))
|
||||
.join(" | ");
|
||||
let msg_log = msg ? msg[0].toUpperCase() + msg.slice(1) + " | " : "";
|
||||
if (detail) {
|
||||
msg_log += detail + " | ";
|
||||
}
|
||||
toast({
|
||||
title: `Request ${type} Failed`,
|
||||
@@ -142,12 +145,15 @@ export const useWrite = <
|
||||
return useMutation({
|
||||
mutationKey: [type],
|
||||
mutationFn: (params: P) => komodo_client().write<T, R>(type, params),
|
||||
onError: (e: { result: { error?: string } }, v, c) => {
|
||||
onError: (e: { result: { error?: string; trace?: string[] } }, v, c) => {
|
||||
console.log("Write error:", e);
|
||||
const msg = e.result.error ?? "Unknown error. See console.";
|
||||
let msg_log = msg ? msg + " - " : "";
|
||||
if (msg_log) {
|
||||
msg_log = msg_log[0].toUpperCase() + msg_log.slice(1);
|
||||
const detail = e.result?.trace
|
||||
?.map((msg) => msg[0].toUpperCase() + msg.slice(1))
|
||||
.join(" | ");
|
||||
let msg_log = msg ? msg[0].toUpperCase() + msg.slice(1) + " | " : "";
|
||||
if (detail) {
|
||||
msg_log += detail + " | ";
|
||||
}
|
||||
toast({
|
||||
title: `Write request ${type} failed`,
|
||||
@@ -176,12 +182,15 @@ export const useExecute = <
|
||||
return useMutation({
|
||||
mutationKey: [type],
|
||||
mutationFn: (params: P) => komodo_client().execute<T, R>(type, params),
|
||||
onError: (e: { result: { error?: string } }, v, c) => {
|
||||
onError: (e: { result: { error?: string; trace?: string[] } }, v, c) => {
|
||||
console.log("Execute error:", e);
|
||||
const msg = e.result.error ?? "Unknown error. See console.";
|
||||
let msg_log = msg ? msg + " | " : "";
|
||||
if (msg_log) {
|
||||
msg_log = msg_log[0].toUpperCase() + msg_log.slice(1);
|
||||
const detail = e.result?.trace
|
||||
?.map((msg) => msg[0].toUpperCase() + msg.slice(1))
|
||||
.join(" | ");
|
||||
let msg_log = msg ? msg[0].toUpperCase() + msg.slice(1) + " | " : "";
|
||||
if (detail) {
|
||||
msg_log += detail + " | ";
|
||||
}
|
||||
toast({
|
||||
title: `Execute request ${type} failed`,
|
||||
@@ -210,12 +219,15 @@ export const useAuth = <
|
||||
return useMutation({
|
||||
mutationKey: [type],
|
||||
mutationFn: (params: P) => komodo_client().auth<T, R>(type, params),
|
||||
onError: (e: { result: { error?: string } }, v, c) => {
|
||||
onError: (e: { result: { error?: string; trace?: string[] } }, v, c) => {
|
||||
console.log("Auth error:", e);
|
||||
const msg = e.result.error ?? "Unknown error. See console.";
|
||||
let msg_log = msg ? msg + " | " : "";
|
||||
if (msg_log) {
|
||||
msg_log = msg_log[0].toUpperCase() + msg_log.slice(1);
|
||||
const detail = e.result?.trace
|
||||
?.map((msg) => msg[0].toUpperCase() + msg.slice(1))
|
||||
.join(" | ");
|
||||
let msg_log = msg ? msg[0].toUpperCase() + msg.slice(1) + " | " : "";
|
||||
if (detail) {
|
||||
msg_log += detail + " | ";
|
||||
}
|
||||
toast({
|
||||
title: `Auth request ${type} failed`,
|
||||
@@ -526,3 +538,31 @@ export const useFilterByUpdateAvailable: () => [boolean, () => void] = () => {
|
||||
const [filter, set] = useAtom<boolean>(filter_by_update_available);
|
||||
return [filter, () => set(!filter)];
|
||||
};
|
||||
|
||||
// export function useReadableLines(stream: ReadableStream<string>): string[] {
|
||||
// const [out, setOut] = useState<string[]>([]);
|
||||
// const cancelRef = useRef<AbortController | null>(null);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!stream) return;
|
||||
|
||||
// const aborter = new AbortController();
|
||||
// cancelRef.current = aborter;
|
||||
// setOut([]); // reset on new stream
|
||||
|
||||
// (async () => {
|
||||
// try {
|
||||
// for await (const line of lines(stream)) {
|
||||
// if (aborter.signal.aborted) break;
|
||||
// setOut((prev) => [...prev, line]); // append as we go
|
||||
// }
|
||||
// } catch (err) {
|
||||
// if (err.name !== "AbortError") console.error(err);
|
||||
// }
|
||||
// })();
|
||||
|
||||
// return () => aborter.abort(); // stop when unmounted
|
||||
// }, [stream]);
|
||||
|
||||
// return out;
|
||||
// }
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
ResourcePageHeader,
|
||||
ShowHideButton,
|
||||
} from "@components/util";
|
||||
import { useRead, useSetTitle, useWrite } from "@lib/hooks";
|
||||
import { useLocalStorage, useRead, useSetTitle, useWrite } from "@lib/hooks";
|
||||
import { Button } from "@ui/button";
|
||||
import { DataTable } from "@ui/data-table";
|
||||
import {
|
||||
@@ -31,6 +31,8 @@ import { useEditPermissions } from "@pages/resource";
|
||||
import { ResourceNotifications } from "@pages/resource-notifications";
|
||||
import { MonacoEditor } from "@components/monaco";
|
||||
import { useState } from "react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs";
|
||||
import { ContainerTerminal } from "@components/terminal";
|
||||
|
||||
export const ContainerPage = () => {
|
||||
const { type, id, container } = useParams() as {
|
||||
@@ -182,8 +184,11 @@ const ContainerPageInner = ({
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{/* Logs */}
|
||||
<ContainerLogs id={id} container_name={container_name} />
|
||||
<LogOrTerminal
|
||||
server={id}
|
||||
container_name={container_name}
|
||||
state={state}
|
||||
/>
|
||||
|
||||
{/* TOP LEVEL CONTAINER INFO */}
|
||||
<Section title="Details" icon={<Info className="w-4 h-4" />}>
|
||||
@@ -229,93 +234,61 @@ const ContainerPageInner = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// return (
|
||||
// <div className="flex flex-col gap-16 mb-24">
|
||||
// {/* HEADER */}
|
||||
// <div className="flex flex-col gap-4">
|
||||
// {/* BACK */}
|
||||
// <div className="flex items-center justify-between mb-4">
|
||||
// <Button
|
||||
// className="gap-2"
|
||||
// variant="secondary"
|
||||
// onClick={() => nav("/servers/" + id)}
|
||||
// >
|
||||
// <ChevronLeft className="w-4" /> Back
|
||||
// </Button>
|
||||
|
||||
// <NewDeployment id={id} container={container_name} />
|
||||
// </div>
|
||||
|
||||
// {/* TITLE */}
|
||||
// <div className="flex items-center gap-4">
|
||||
// <div className="mt-1">
|
||||
// <DOCKER_LINK_ICONS.container
|
||||
// server_id={id}
|
||||
// name={container_name}
|
||||
// size={8}
|
||||
// />
|
||||
// </div>
|
||||
// <DockerResourcePageName name={container_name} />
|
||||
// <div className="flex items-center gap-4 flex-wrap">
|
||||
// <StatusBadge
|
||||
// text={state}
|
||||
// intent={container_state_intention(state)}
|
||||
// />
|
||||
// {status && (
|
||||
// <p className="text-sm text-muted-foreground">{status}</p>
|
||||
// )}
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// {/* INFO */}
|
||||
// <div className="flex flex-wrap gap-4 items-center text-muted-foreground">
|
||||
// <ResourceLink type="Server" id={id} />
|
||||
// <AttachedResource id={id} container={container_name} />
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// {/* Actions */}
|
||||
// {canExecute && (
|
||||
// <Section title="Actions" icon={<Clapperboard className="w-4 h-4" />}>
|
||||
// <div className="flex gap-4 items-center flex-wrap">
|
||||
// {Object.entries(Actions).map(([key, Action]) => (
|
||||
// <Action key={key} id={id} container={container_name} />
|
||||
// ))}
|
||||
// </div>
|
||||
// </Section>
|
||||
// )}
|
||||
|
||||
// {/* Updates */}
|
||||
// <ResourceUpdates type="Server" id={id} />
|
||||
|
||||
// <ContainerLogs id={id} container_name={container_name} />
|
||||
|
||||
// {/* TOP LEVEL CONTAINER INFO */}
|
||||
// <Section title="Details" icon={<Info className="w-4 h-4" />}>
|
||||
// <DataTable
|
||||
// tableKey="container-info"
|
||||
// data={[container]}
|
||||
// columns={[
|
||||
// {
|
||||
// accessorKey: "Id",
|
||||
// header: "Id",
|
||||
// },
|
||||
// {
|
||||
// accessorKey: "Image",
|
||||
// header: "Image",
|
||||
// },
|
||||
// {
|
||||
// accessorKey: "Driver",
|
||||
// header: "Driver",
|
||||
// },
|
||||
// ]}
|
||||
// />
|
||||
// </Section>
|
||||
|
||||
// <DockerLabelsSection labels={container.Config?.Labels} />
|
||||
// </div>
|
||||
// );
|
||||
const LogOrTerminal = ({
|
||||
server,
|
||||
container_name,
|
||||
state,
|
||||
}: {
|
||||
server: string;
|
||||
container_name: string;
|
||||
state: Types.ContainerStateStatusEnum;
|
||||
}) => {
|
||||
const [_view, setView] = useLocalStorage<"Log" | "Terminal">(
|
||||
`server-${server}-${container_name}-tabs-v1`,
|
||||
"Log"
|
||||
);
|
||||
const { canWrite } = useEditPermissions({
|
||||
type: "Server",
|
||||
id: server,
|
||||
});
|
||||
const terminals_disabled = useServer(server)?.info.terminals_disabled ?? true;
|
||||
const terminalDisabled =
|
||||
!canWrite ||
|
||||
terminals_disabled ||
|
||||
state !== Types.ContainerStateStatusEnum.Running;
|
||||
const view = terminalDisabled && _view === "Terminal" ? "Log" : _view;
|
||||
const tabs = (
|
||||
<TabsList className="justify-start w-fit">
|
||||
<TabsTrigger value="Log" className="w-[110px]">
|
||||
Log
|
||||
</TabsTrigger>
|
||||
{!terminalDisabled && (
|
||||
<TabsTrigger value="Terminal" className="w-[110px]">
|
||||
Terminal
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
);
|
||||
return (
|
||||
<Tabs value={view} onValueChange={setView as any} className="grid gap-4">
|
||||
<TabsContent value="Log">
|
||||
<ContainerLogs
|
||||
id={server}
|
||||
container_name={container_name}
|
||||
titleOther={tabs}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="Terminal">
|
||||
<ContainerTerminal
|
||||
server={server}
|
||||
container_name={container_name}
|
||||
titleOther={tabs}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
const AttachedResource = ({
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { Log, LogSection } from "@components/log";
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { Types } from "komodo_client";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export const ContainerLogs = ({
|
||||
id,
|
||||
container_name,
|
||||
titleOther,
|
||||
}: {
|
||||
/// Server id
|
||||
id: string;
|
||||
container_name: string;
|
||||
titleOther?: ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<LogSection
|
||||
titleOther={titleOther}
|
||||
regular_logs={(timestamps, stream, tail) =>
|
||||
NoSearchLogs(id, container_name, tail, timestamps, stream)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
container_state_intention,
|
||||
stroke_color_class_by_intention,
|
||||
} from "@lib/color";
|
||||
import { useRead, useSetTitle } from "@lib/hooks";
|
||||
import { useLocalStorage, useRead, useSetTitle } from "@lib/hooks";
|
||||
import { cn } from "@lib/utils";
|
||||
import { Types } from "komodo_client";
|
||||
import { ChevronLeft, Clapperboard, Layers2 } from "lucide-react";
|
||||
@@ -28,6 +28,9 @@ import { DockerResourceLink, ResourcePageHeader } from "@components/util";
|
||||
import { useEditPermissions } from "@pages/resource";
|
||||
import { ResourceNotifications } from "@pages/resource-notifications";
|
||||
import { Fragment } from "react/jsx-runtime";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs";
|
||||
import { ContainerTerminal } from "@components/terminal";
|
||||
import { useServer } from "@components/resources/server";
|
||||
|
||||
type IdServiceComponent = React.FC<{ id: string; service?: string }>;
|
||||
|
||||
@@ -183,9 +186,72 @@ const StackServicePageInner = ({
|
||||
|
||||
{/* Logs */}
|
||||
<div className="pt-4">
|
||||
<StackServiceLogs id={stack_id} service={service} />
|
||||
{stack && (
|
||||
<LogOrTerminal
|
||||
stack={stack}
|
||||
service={service}
|
||||
container_name={container?.name}
|
||||
container_state={state}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LogOrTerminal = ({
|
||||
stack,
|
||||
service,
|
||||
container_name,
|
||||
container_state,
|
||||
}: {
|
||||
stack: Types.StackListItem;
|
||||
service: string;
|
||||
container_name: string | undefined;
|
||||
container_state: Types.ContainerStateStatusEnum;
|
||||
}) => {
|
||||
const [_view, setView] = useLocalStorage<"Log" | "Terminal">(
|
||||
`stack-${stack.id}-${service}-tabs-v1`,
|
||||
"Log"
|
||||
);
|
||||
const { canWrite: canWriteServer } = useEditPermissions({
|
||||
type: "Server",
|
||||
id: stack.info.server_id,
|
||||
});
|
||||
const terminals_disabled =
|
||||
useServer(stack.info.server_id)?.info.terminals_disabled ?? true;
|
||||
const terminalDisabled =
|
||||
!canWriteServer ||
|
||||
terminals_disabled ||
|
||||
container_state !== Types.ContainerStateStatusEnum.Running;
|
||||
const view = terminalDisabled && _view === "Terminal" ? "Log" : _view;
|
||||
const tabs = (
|
||||
<TabsList className="justify-start w-fit">
|
||||
<TabsTrigger value="Log" className="w-[110px]">
|
||||
Log
|
||||
</TabsTrigger>
|
||||
{!terminalDisabled && (
|
||||
<TabsTrigger value="Terminal" className="w-[110px]">
|
||||
Terminal
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
);
|
||||
return (
|
||||
<Tabs value={view} onValueChange={setView as any} className="grid gap-4">
|
||||
<TabsContent value="Log">
|
||||
<StackServiceLogs id={stack.id} service={service} titleOther={tabs} />
|
||||
</TabsContent>
|
||||
<TabsContent value="Terminal">
|
||||
{stack.info.server_id && container_name && (
|
||||
<ContainerTerminal
|
||||
server={stack.info.server_id}
|
||||
container_name={container_name}
|
||||
titleOther={tabs}
|
||||
/>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { Types } from "komodo_client";
|
||||
import { Log, LogSection } from "@components/log";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export const StackServiceLogs = ({
|
||||
id,
|
||||
service,
|
||||
titleOther,
|
||||
}: {
|
||||
/// Stack id
|
||||
id: string;
|
||||
service: string;
|
||||
titleOther?: ReactNode;
|
||||
}) => {
|
||||
// const stack = useStack(id);
|
||||
const services = useRead("ListStackServices", { stack: id }).data;
|
||||
@@ -19,19 +22,22 @@ export const StackServiceLogs = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
return <StackLogsInner id={id} service={service} />;
|
||||
return <StackLogsInner titleOther={titleOther} id={id} service={service} />;
|
||||
};
|
||||
|
||||
const StackLogsInner = ({
|
||||
id,
|
||||
service,
|
||||
titleOther,
|
||||
}: {
|
||||
/// Stack id
|
||||
id: string;
|
||||
service: string;
|
||||
titleOther?: ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<LogSection
|
||||
titleOther={titleOther}
|
||||
regular_logs={(timestamps, stream, tail) =>
|
||||
NoSearchLogs(id, service, tail, timestamps, stream)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
@@ -13,18 +16,26 @@
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitAny": false,
|
||||
|
||||
/* Paths */
|
||||
"baseUrl": "./src",
|
||||
"paths": { "@*": ["./*"] }
|
||||
"paths": {
|
||||
"@*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1447,6 +1447,16 @@
|
||||
"@types/babel__core" "^7.20.5"
|
||||
react-refresh "^0.14.2"
|
||||
|
||||
"@xterm/addon-fit@^0.10.0":
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@xterm/addon-fit/-/addon-fit-0.10.0.tgz#bebf87fadd74e3af30fdcdeef47030e2592c6f55"
|
||||
integrity sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==
|
||||
|
||||
"@xterm/xterm@^5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.5.0.tgz#275fb8f6e14afa6e8a0c05d4ebc94523ff775396"
|
||||
integrity sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==
|
||||
|
||||
acorn-jsx@^5.3.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||
@@ -2875,6 +2885,11 @@ react-style-singleton@^2.2.2, react-style-singleton@^2.2.3:
|
||||
get-nonce "^1.0.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
react-xtermjs@^1.0.10:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/react-xtermjs/-/react-xtermjs-1.0.10.tgz#faac189f60ca599345b69b5c1a6b662f537df667"
|
||||
integrity sha512-+xpKEKbmsypWzRKE0FR1LNIGcI8gx+R6VMHe8IQW7iTbgeqp3Qg7SbiVNOzR+Ovb1QK4DPA3KqsIQV+XP0iRUA==
|
||||
|
||||
react@19.0.0:
|
||||
version "19.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-19.0.0.tgz#6e1969251b9f108870aa4bff37a0ce9ddfaaabdd"
|
||||
|
||||
Reference in New Issue
Block a user